From 4f4aca2d967267055e3c4953fad8be565edee708 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Tue, 3 Jun 2025 17:08:42 -0400 Subject: [PATCH 01/46] Started on async --- pixi.lock | 541 +++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + src/cditools/eiger_async.py | 68 +++++ 3 files changed, 608 insertions(+), 2 deletions(-) create mode 100644 src/cditools/eiger_async.py diff --git a/pixi.lock b/pixi.lock index dc42997d..5b87dec4 100644 --- a/pixi.lock +++ b/pixi.lock @@ -29,19 +29,46 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fc/30/f7da949893bf0d8525acbaee256cf55430332852225f17e18a7f8783dd6f/bluesky-1.14.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-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/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b1/f2/47e3f0e06c3bc3834db2a4ed01641e1ce493108461b9540dd5d2fb60d334/event_model-1.22.3-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/fe/5e/3be305568fe5f34448807976dc82fc151d76c3e0e03958f34770286278c1/flexparser-0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fd/41/bc2df86b72965775f6d621e0ee269a5f3ac23e8f870abf519de9c7d93b4d/h5py-3.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_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/79/9d/0fb148dc4d6fa4a7dd1d8378168d9b4cd8d4560a6fbf6f0121c5fc34eb68/importlib_metadata-8.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/97/12/a1f2f4fdc6b7159c0d12249456f9fe454665b6126e98dbee9f2bd3cf735c/lz4-4.4.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2d/7b/2c1d74ca6c94f70a1add74a8393a0138172207dc5de6fc6269483519d048/msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.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/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/fc/ebfd32c3e124e6a1043e19c0ab0769818aa69050ce5589b63d05ff185526/numpy-2.2.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/12/f2/89ea3361a305466bc6460a532188830351220b5f0851a5fa133155c16eca/opentelemetry_api-1.32.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/5e/f5ded172af40b7e7ae60697a2b2096cec53e45344edce41e91a74425473c/ophyd-1.10.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/85/38/6b455b1d6f0b9aa3db4e8888b797f77d856a8350d836c66a43d1078573b2/ophyd_async-0.10.0a4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/16/bd2f5904557265882108dc2e04f18abc05ab0c2b7082ae9430091daf1d5c/Pint-0.24.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.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/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/05/7c/8abb70f9017a231c6c961a8941403ed6557664c0913e1bf413cbdc039e75/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/04/9e/91fe10ea09e2383f0d0531cf3da765fa01b3b9918e85df92081813259cf4/ruamel.yaml-0.18.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1f/8f/ecfbe2123ade605c49ef769788f79c38ddb1c8fa81e01f4dbf5cf1a44b16/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/03/98/eb27cc78ad3af8e302c9d8ff4977f5026676e130d28dd7578132a457170c/toolz-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl - pypi: . @@ -74,32 +101,47 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fc/30/f7da949893bf0d8525acbaee256cf55430332852225f17e18a7f8783dd6f/bluesky-1.14.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e9/ad/0ce6cef4823d6c44b87e9b26b2cb284f6e25143ea0ab8215ac836bda825e/caproto-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-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/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/11/79/479e2194c9096b92aecdf33634ae948d2be306c6011673e98ee1917f32c2/dpkt-1.9.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b1/f2/47e3f0e06c3bc3834db2a4ed01641e1ce493108461b9540dd5d2fb60d334/event_model-1.22.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.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/fe/5e/3be305568fe5f34448807976dc82fc151d76c3e0e03958f34770286278c1/flexparser-0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fd/41/bc2df86b72965775f6d621e0ee269a5f3ac23e8f870abf519de9c7d93b4d/h5py-3.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_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/2b/d3/85feeba1d097b81a44bcffa6a0beab7b4dfffe78e82fc54978d3ac380736/identify-2.6.10-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/9d/0fb148dc4d6fa4a7dd1d8378168d9b4cd8d4560a6fbf6f0121c5fc34eb68/importlib_metadata-8.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/9d/4ff2adf55d1b6e3777b0303fdbe5b723f76e46cba4a53a32fe82260d2077/ipython-9.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/97/12/a1f2f4fdc6b7159c0d12249456f9fe454665b6126e98dbee9f2bd3cf735c/lz4-4.4.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/7b/2c1d74ca6c94f70a1add74a8393a0138172207dc5de6fc6269483519d048/msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.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/a6/91/86a6eac449ddfae239e93ffc1918cf33fd9bab35c04d1e963b311e347a73/netifaces-0.11.0.tar.gz - pypi: https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/fc/ebfd32c3e124e6a1043e19c0ab0769818aa69050ce5589b63d05ff185526/numpy-2.2.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/12/f2/89ea3361a305466bc6460a532188830351220b5f0851a5fa133155c16eca/opentelemetry_api-1.32.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/5e/f5ded172af40b7e7ae60697a2b2096cec53e45344edce41e91a74425473c/ophyd-1.10.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/85/38/6b455b1d6f0b9aa3db4e8888b797f77d856a8350d836c66a43d1078573b2/ophyd_async-0.10.0a4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl @@ -111,17 +153,28 @@ environments: - pypi: https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.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/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/05/7c/8abb70f9017a231c6c961a8941403ed6557664c0913e1bf413cbdc039e75/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/04/9e/91fe10ea09e2383f0d0531cf3da765fa01b3b9918e85df92081813259cf4/ruamel.yaml-0.18.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1f/8f/ecfbe2123ade605c49ef769788f79c38ddb1c8fa81e01f4dbf5cf1a44b16/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/0e/2c/1e364cc92970075d7d04c69c928430b23e43a433f044474f57e425cbed37/ruff-0.11.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/03/98/eb27cc78ad3af8e302c9d8ff4977f5026676e130d28dd7578132a457170c/toolz-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4c/ed/3cfeb48175f0671ec430ede81f628f9fb2b1084c9064ca67ebe8c0ed6a05/virtualenv-20.30.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl @@ -158,36 +211,59 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - pypi: https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fc/30/f7da949893bf0d8525acbaee256cf55430332852225f17e18a7f8783dd6f/bluesky-1.14.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-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/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b1/f2/47e3f0e06c3bc3834db2a4ed01641e1ce493108461b9540dd5d2fb60d334/event_model-1.22.3-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/fe/5e/3be305568fe5f34448807976dc82fc151d76c3e0e03958f34770286278c1/flexparser-0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/27/48/e791a7ed487dbb9729ef32bb5d1af16693d8925f4366befef54119b2e576/furo-2024.8.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fd/41/bc2df86b72965775f6d621e0ee269a5f3ac23e8f870abf519de9c7d93b4d/h5py-3.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_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/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/9d/0fb148dc4d6fa4a7dd1d8378168d9b4cd8d4560a6fbf6f0121c5fc34eb68/importlib_metadata-8.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/97/12/a1f2f4fdc6b7159c0d12249456f9fe454665b6126e98dbee9f2bd3cf735c/lz4-4.4.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a7/f7/7782a043553ee469c1ff49cfa1cdace2d6bf99a1f333cf38676b3ddf30da/mdit_py_plugins-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/7b/2c1d74ca6c94f70a1add74a8393a0138172207dc5de6fc6269483519d048/msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.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/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/fc/ebfd32c3e124e6a1043e19c0ab0769818aa69050ce5589b63d05ff185526/numpy-2.2.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/12/f2/89ea3361a305466bc6460a532188830351220b5f0851a5fa133155c16eca/opentelemetry_api-1.32.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/5e/f5ded172af40b7e7ae60697a2b2096cec53e45344edce41e91a74425473c/ophyd-1.10.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/85/38/6b455b1d6f0b9aa3db4e8888b797f77d856a8350d836c66a43d1078573b2/ophyd_async-0.10.0a4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/16/bd2f5904557265882108dc2e04f18abc05ab0c2b7082ae9430091daf1d5c/Pint-0.24.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.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/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/05/7c/8abb70f9017a231c6c961a8941403ed6557664c0913e1bf413cbdc039e75/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/04/9e/91fe10ea09e2383f0d0531cf3da765fa01b3b9918e85df92081813259cf4/ruamel.yaml-0.18.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1f/8f/ecfbe2123ade605c49ef769788f79c38ddb1c8fa81e01f4dbf5cf1a44b16/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl @@ -200,7 +276,10 @@ environments: - 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/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-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/03/98/eb27cc78ad3af8e302c9d8ff4977f5026676e130d28dd7578132a457170c/toolz-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl @@ -234,27 +313,54 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fc/30/f7da949893bf0d8525acbaee256cf55430332852225f17e18a7f8783dd6f/bluesky-1.14.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e9/ad/0ce6cef4823d6c44b87e9b26b2cb284f6e25143ea0ab8215ac836bda825e/caproto-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-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/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/11/79/479e2194c9096b92aecdf33634ae948d2be306c6011673e98ee1917f32c2/dpkt-1.9.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b1/f2/47e3f0e06c3bc3834db2a4ed01641e1ce493108461b9540dd5d2fb60d334/event_model-1.22.3-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/fe/5e/3be305568fe5f34448807976dc82fc151d76c3e0e03958f34770286278c1/flexparser-0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fd/41/bc2df86b72965775f6d621e0ee269a5f3ac23e8f870abf519de9c7d93b4d/h5py-3.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_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/79/9d/0fb148dc4d6fa4a7dd1d8378168d9b4cd8d4560a6fbf6f0121c5fc34eb68/importlib_metadata-8.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/97/12/a1f2f4fdc6b7159c0d12249456f9fe454665b6126e98dbee9f2bd3cf735c/lz4-4.4.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2d/7b/2c1d74ca6c94f70a1add74a8393a0138172207dc5de6fc6269483519d048/msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.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/a6/91/86a6eac449ddfae239e93ffc1918cf33fd9bab35c04d1e963b311e347a73/netifaces-0.11.0.tar.gz - pypi: https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/fc/ebfd32c3e124e6a1043e19c0ab0769818aa69050ce5589b63d05ff185526/numpy-2.2.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/12/f2/89ea3361a305466bc6460a532188830351220b5f0851a5fa133155c16eca/opentelemetry_api-1.32.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/5e/f5ded172af40b7e7ae60697a2b2096cec53e45344edce41e91a74425473c/ophyd-1.10.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/85/38/6b455b1d6f0b9aa3db4e8888b797f77d856a8350d836c66a43d1078573b2/ophyd_async-0.10.0a4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/16/bd2f5904557265882108dc2e04f18abc05ab0c2b7082ae9430091daf1d5c/Pint-0.24.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.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/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/05/7c/8abb70f9017a231c6c961a8941403ed6557664c0913e1bf413cbdc039e75/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/04/9e/91fe10ea09e2383f0d0531cf3da765fa01b3b9918e85df92081813259cf4/ruamel.yaml-0.18.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1f/8f/ecfbe2123ade605c49ef769788f79c38ddb1c8fa81e01f4dbf5cf1a44b16/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/03/98/eb27cc78ad3af8e302c9d8ff4977f5026676e130d28dd7578132a457170c/toolz-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl - pypi: . @@ -285,6 +391,13 @@ packages: version: 1.0.0 sha256: fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + name: annotated-types + version: 0.7.0 + sha256: 1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 + requires_dist: + - typing-extensions>=4.0.0 ; python_full_version < '3.9' + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl name: asttokens version: 3.0.0 @@ -296,6 +409,52 @@ packages: - pytest-cov ; extra == 'test' - pytest-xdist ; extra == 'test' requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl + name: attrs + version: 25.3.0 + sha256: 427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3 + requires_dist: + - cloudpickle ; platform_python_implementation == 'CPython' and extra == 'benchmark' + - hypothesis ; extra == 'benchmark' + - mypy>=1.11.1 ; python_full_version >= '3.10' and platform_python_implementation == 'CPython' and extra == 'benchmark' + - pympler ; extra == 'benchmark' + - pytest-codspeed ; extra == 'benchmark' + - pytest-mypy-plugins ; python_full_version >= '3.10' and platform_python_implementation == 'CPython' and extra == 'benchmark' + - pytest-xdist[psutil] ; extra == 'benchmark' + - pytest>=4.3.0 ; extra == 'benchmark' + - cloudpickle ; platform_python_implementation == 'CPython' and extra == 'cov' + - coverage[toml]>=5.3 ; extra == 'cov' + - hypothesis ; extra == 'cov' + - mypy>=1.11.1 ; python_full_version >= '3.10' and platform_python_implementation == 'CPython' and extra == 'cov' + - pympler ; extra == 'cov' + - pytest-mypy-plugins ; python_full_version >= '3.10' and platform_python_implementation == 'CPython' and extra == 'cov' + - pytest-xdist[psutil] ; extra == 'cov' + - pytest>=4.3.0 ; extra == 'cov' + - cloudpickle ; platform_python_implementation == 'CPython' and extra == 'dev' + - hypothesis ; extra == 'dev' + - mypy>=1.11.1 ; python_full_version >= '3.10' and platform_python_implementation == 'CPython' and extra == 'dev' + - pre-commit-uv ; extra == 'dev' + - pympler ; extra == 'dev' + - pytest-mypy-plugins ; python_full_version >= '3.10' and platform_python_implementation == 'CPython' and extra == 'dev' + - pytest-xdist[psutil] ; extra == 'dev' + - pytest>=4.3.0 ; extra == 'dev' + - cogapp ; extra == 'docs' + - furo ; extra == 'docs' + - myst-parser ; extra == 'docs' + - sphinx ; extra == 'docs' + - sphinx-notfound-page ; extra == 'docs' + - sphinxcontrib-towncrier ; extra == 'docs' + - towncrier ; extra == 'docs' + - cloudpickle ; platform_python_implementation == 'CPython' and extra == 'tests' + - hypothesis ; extra == 'tests' + - mypy>=1.11.1 ; python_full_version >= '3.10' and platform_python_implementation == 'CPython' and extra == 'tests' + - pympler ; extra == 'tests' + - pytest-mypy-plugins ; python_full_version >= '3.10' and platform_python_implementation == 'CPython' and extra == 'tests' + - pytest-xdist[psutil] ; extra == 'tests' + - pytest>=4.3.0 ; extra == 'tests' + - mypy>=1.11.1 ; python_full_version >= '3.10' and platform_python_implementation == 'CPython' and extra == 'tests-mypy' + - pytest-mypy-plugins ; python_full_version >= '3.10' and platform_python_implementation == 'CPython' and extra == 'tests-mypy' + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl name: babel version: 2.17.0 @@ -324,6 +483,90 @@ packages: - html5lib ; extra == 'html5lib' - lxml ; extra == 'lxml' requires_python: '>=3.7.0' +- pypi: https://files.pythonhosted.org/packages/fc/30/f7da949893bf0d8525acbaee256cf55430332852225f17e18a7f8783dd6f/bluesky-1.14.1-py3-none-any.whl + name: bluesky + version: 1.14.1 + sha256: e496fa9e6ef055eb0cb5ee4580abeb7e2b2bcc1e18c5ba2904819a7a42e7ff0d + requires_dist: + - cycler + - event-model>=1.19.8 + - historydict + - msgpack + - msgpack-numpy + - numpy + - opentelemetry-api + - toolz + - tqdm>=4.44 + - typing-extensions>=4.10.0 + - attrs ; extra == 'dev' + - cloudpickle ; extra == 'dev' + - copier ; extra == 'dev' + - coverage ; extra == 'dev' + - databroker ; extra == 'dev' + - doct ; extra == 'dev' + - doctr ; extra == 'dev' + - flake8 ; extra == 'dev' + - ipython ; extra == 'dev' + - ipywidgets ; extra == 'dev' + - jinja2 ; extra == 'dev' + - lmfit ; extra == 'dev' + - matplotlib>=3.5.0 ; extra == 'dev' + - mongoquery ; extra == 'dev' + - multiprocess ; extra == 'dev' + - mypy ; extra == 'dev' + - myst-parser ; extra == 'dev' + - networkx ; extra == 'dev' + - numpydoc ; extra == 'dev' + - ophyd ; extra == 'dev' + - orjson ; extra == 'dev' + - packaging ; extra == 'dev' + - pandas ; extra == 'dev' + - pickleshare ; extra == 'dev' + - pipdeptree ; extra == 'dev' + - pre-commit ; extra == 'dev' + - pydata-sphinx-theme>=0.12 ; extra == 'dev' + - pyepics<=3.5.2 ; python_full_version < '3.9' and extra == 'dev' + - pyepics ; python_full_version >= '3.9' and extra == 'dev' + - pyqt5 ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - pytest-faulthandler ; extra == 'dev' + - pyyaml ; extra == 'dev' + - pyzmq ; extra == 'dev' + - requests ; extra == 'dev' + - ruff ; extra == 'dev' + - scikit-image ; extra == 'dev' + - scipy ; extra == 'dev' + - sphinx<7.3 ; extra == 'dev' + - sphinx-autobuild ; extra == 'dev' + - sphinx-copybutton ; extra == 'dev' + - sphinx-design ; extra == 'dev' + - sphinx-rtd-theme ; extra == 'dev' + - streamz ; extra == 'dev' + - suitcase-jsonl ; extra == 'dev' + - suitcase-mongo ; extra == 'dev' + - suitcase-msgpack ; extra == 'dev' + - tifffile ; extra == 'dev' + - tox-direct ; extra == 'dev' + - types-mock ; extra == 'dev' + - tiled[all] ; extra == 'dev' + - vendoring ; extra == 'dev' + - zict ; extra == 'dev' + - ipython ; extra == 'ipython' + - pyzmq ; extra == 'zmq' + - ophyd ; extra == 'common' + - databroker ; extra == 'common' + - doct ; extra == 'tools' + - lmfit ; extra == 'tools' + - tifffile ; extra == 'tools' + - historydict ; extra == 'tools' + - streamz ; extra == 'streamz' + - matplotlib ; extra == 'plotting' + - colorama ; extra == 'cmd' + - jinja2 ; extra == 'olog' + - zict<3 ; extra == 'old-persistentdict' + - bluesky[cmd,common,dev,ipython,olog,plotting,streamz,tools,zmq] ; extra == 'all' + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda sha256: 5ced96500d945fb286c9c838e54fa759aa04a7129c59800f0846b4335cee770d md5: 62ee74e96c5ebb0af99386de58cf9553 @@ -379,10 +622,11 @@ packages: requires_python: '>=3.8' - pypi: . name: cditools - version: 0.1.dev18+g22d45e0.d20250515 - sha256: 2a690b8e82f56c90db4126ff52415e1fca013a04ccaa3f4e60594e428a8b7b74 + version: 0.1.dev23+g6e8b9de.d20250603 + sha256: a283f4a7570f79bc9f22553e6cb1ffc12ec0385473c900d81185dbc46516f800 requires_dist: - ophyd + - ophyd-async>=0.10.0a4 - h5py - pytest>=6 ; extra == 'test' - pytest-cov>=3 ; extra == 'test' @@ -416,6 +660,30 @@ packages: version: 3.4.1 sha256: 955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11 requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-py3-none-any.whl + name: colorlog + version: 6.9.0 + sha256: 5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff + requires_dist: + - colorama ; sys_platform == 'win32' + - black ; extra == 'development' + - flake8 ; extra == 'development' + - mypy ; extra == 'development' + - pytest ; extra == 'development' + - types-colorama ; extra == 'development' + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/f3/4f/f94ac1b84d2169cf2ebf64353ce98fd743f85d30678059c514d9b3d6644c/compress_pickle-2.1.0-py3-none-any.whl + name: compress-pickle + version: 2.1.0 + sha256: 598650da4686d9bd97bee185b61e74d7fe1872bb0c23909d5ed2d8793b4a8818 + requires_dist: + - cloudpickle ; extra == 'cloudpickle' + - dill ; extra == 'dill' + - lz4 ; extra == 'full' + - dill ; extra == 'full' + - cloudpickle ; extra == 'full' + - lz4 ; extra == 'lz4' + requires_python: '>=3.6' - pypi: https://files.pythonhosted.org/packages/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl name: coverage version: 7.8.0 @@ -423,6 +691,19 @@ packages: requires_dist: - tomli ; python_full_version <= '3.11' and extra == 'toml' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + name: cycler + version: 0.12.1 + sha256: 85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30 + requires_dist: + - ipython ; extra == 'docs' + - matplotlib ; extra == 'docs' + - numpydoc ; extra == 'docs' + - sphinx ; extra == 'docs' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl name: decorator version: 5.2.1 @@ -453,6 +734,34 @@ packages: name: dpkt version: 1.9.8 sha256: 4da4d111d7bf67575b571f5c678c71bddd2d8a01a3d57d489faf0a92c748fbfd +- pypi: https://files.pythonhosted.org/packages/b1/f2/47e3f0e06c3bc3834db2a4ed01641e1ce493108461b9540dd5d2fb60d334/event_model-1.22.3-py3-none-any.whl + name: event-model + version: 1.22.3 + sha256: 265f6730d35dc181bc6880e4d3ee94a888c39b1361a3f7bb12f4090acc538dad + requires_dist: + - importlib-resources + - jsonschema>=4 + - numpy + - typing-extensions + - mypy ; extra == 'dev' + - myst-parser ; extra == 'dev' + - pipdeptree ; extra == 'dev' + - pre-commit ; extra == 'dev' + - pydata-sphinx-theme>=0.12 ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - ruff ; extra == 'dev' + - sphinx-autobuild ; extra == 'dev' + - sphinx-copybutton ; extra == 'dev' + - sphinx-design ; extra == 'dev' + - tox-direct ; extra == 'dev' + - types-mock ; extra == 'dev' + - copier ; extra == 'dev' + - ipython ; extra == 'dev' + - matplotlib ; extra == 'dev' + - numpydoc ; extra == 'dev' + - pydantic>=2.6 ; extra == 'dev' + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl name: executing version: 2.2.0 @@ -524,6 +833,10 @@ packages: requires_dist: - numpy>=1.19.3 requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/fe/47/deb64c73aec25af7699247e021153a6bfe9a08452f7f7337dcee4aa07a2b/historydict-1.2.6-py3-none-any.whl + name: historydict + version: 1.2.6 + sha256: b4b00a170f05502aa682caba62435da5fe1f73037e884707581fe84f8d7b43f5 - pypi: https://files.pythonhosted.org/packages/2b/d3/85feeba1d097b81a44bcffa6a0beab7b4dfffe78e82fc54978d3ac380736/identify-2.6.10-py2.py3-none-any.whl name: identify version: 2.6.10 @@ -573,6 +886,27 @@ packages: - pytest-enabler>=2.2 ; extra == 'enabler' - pytest-mypy ; extra == 'type' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl + name: importlib-resources + version: 6.5.2 + sha256: 789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec + requires_dist: + - zipp>=3.1.0 ; python_full_version < '3.10' + - pytest>=6,!=8.1.* ; extra == 'test' + - zipp>=3.17 ; extra == 'test' + - jaraco-test>=5.4 ; extra == 'test' + - sphinx>=3.5 ; extra == 'doc' + - jaraco-packaging>=9.3 ; extra == 'doc' + - rst-linker>=1.9 ; extra == 'doc' + - furo ; extra == 'doc' + - sphinx-lint ; extra == 'doc' + - jaraco-tidelift>=1.4 ; extra == 'doc' + - pytest-checkdocs>=2.4 ; extra == 'check' + - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' + - pytest-cov ; extra == 'cover' + - pytest-enabler>=2.2 ; extra == 'enabler' + - pytest-mypy ; extra == 'type' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl name: iniconfig version: 2.1.0 @@ -678,6 +1012,41 @@ packages: - markupsafe>=2.0 - babel>=2.7 ; extra == 'i18n' requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl + name: jsonschema + version: 4.24.0 + sha256: a462455f19f5faf404a7902952b6f0e3ce868f3ee09a359b05eca6673bd8412d + requires_dist: + - attrs>=22.2.0 + - importlib-resources>=1.4.0 ; python_full_version < '3.9' + - jsonschema-specifications>=2023.3.6 + - pkgutil-resolve-name>=1.3.10 ; python_full_version < '3.9' + - referencing>=0.28.4 + - rpds-py>=0.7.1 + - fqdn ; extra == 'format' + - idna ; extra == 'format' + - isoduration ; extra == 'format' + - jsonpointer>1.13 ; extra == 'format' + - rfc3339-validator ; extra == 'format' + - rfc3987 ; extra == 'format' + - uri-template ; extra == 'format' + - webcolors>=1.11 ; extra == 'format' + - fqdn ; extra == 'format-nongpl' + - idna ; extra == 'format-nongpl' + - isoduration ; extra == 'format-nongpl' + - jsonpointer>1.13 ; extra == 'format-nongpl' + - rfc3339-validator ; extra == 'format-nongpl' + - rfc3986-validator>0.1.0 ; extra == 'format-nongpl' + - uri-template ; extra == 'format-nongpl' + - webcolors>=24.6.0 ; extra == 'format-nongpl' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl + name: jsonschema-specifications + version: 2025.4.1 + sha256: 4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af + requires_dist: + - referencing>=0.31.0 + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_4.conda sha256: db73f38155d901a610b2320525b9dd3b31e4949215c870685fd92ea61b5ce472 md5: 01f8d123c96816249efd255a31ad7712 @@ -803,6 +1172,18 @@ packages: purls: [] size: 60963 timestamp: 1727963148474 +- pypi: https://files.pythonhosted.org/packages/97/12/a1f2f4fdc6b7159c0d12249456f9fe454665b6126e98dbee9f2bd3cf735c/lz4-4.4.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: lz4 + version: 4.4.4 + sha256: e9cb387c33f014dae4db8cb4ba789c8d2a0a6d045ddff6be13f6c8d9def1d2a6 + requires_dist: + - pytest!=3.3.0 ; extra == 'tests' + - psutil ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - sphinx>=1.6.0 ; extra == 'docs' + - sphinx-bootstrap-theme ; extra == 'docs' + - flake8 ; extra == 'flake8' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl name: markdown-it-py version: 3.0.0 @@ -865,6 +1246,18 @@ packages: version: 0.1.2 sha256: 84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/2d/7b/2c1d74ca6c94f70a1add74a8393a0138172207dc5de6fc6269483519d048/msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: msgpack + version: 1.1.0 + sha256: 0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca + 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 + sha256: 773c19d4dfbae1b3c7b791083e2caf66983bb19b40901646f61d8731554ae3da + requires_dist: + - numpy>=1.9.0 + - msgpack>=0.5.2 - pypi: https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl name: myst-parser version: 4.0.1 @@ -1025,6 +1418,58 @@ packages: - sphinx-design ; extra == 'dev' - tox-direct ; extra == 'dev' requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/85/38/6b455b1d6f0b9aa3db4e8888b797f77d856a8350d836c66a43d1078573b2/ophyd_async-0.10.0a4-py3-none-any.whl + name: ophyd-async + version: 0.10.0a4 + sha256: 00992baf393b95f707b332aa74d2b1b82453c4bdf015b52d5f419920d768f878 + requires_dist: + - numpy + - bluesky>=1.13.1rc2 + - event-model>=1.22.1 + - pyyaml + - colorlog + - pydantic>=2.0 + - pydantic-numpy + - h5py ; extra == 'sim' + - aioca>=1.6 ; extra == 'ca' + - p4p>=4.2.0 ; extra == 'pva' + - pytango==10.0.0 ; extra == 'tango' + - ipython ; extra == 'demo' + - matplotlib ; extra == 'demo' + - pyqt6 ; extra == 'demo' + - ophyd-async[sim] ; extra == 'dev' + - ophyd-async[ca] ; extra == 'dev' + - ophyd-async[pva] ; extra == 'dev' + - ophyd-async[tango] ; extra == 'dev' + - ophyd-async[demo] ; extra == 'dev' + - inflection ; extra == 'dev' + - import-linter ; extra == 'dev' + - myst-parser ; extra == 'dev' + - numpydoc ; extra == 'dev' + - ophyd ; extra == 'dev' + - pickleshare ; extra == 'dev' + - pipdeptree ; extra == 'dev' + - pre-commit ; extra == 'dev' + - pydata-sphinx-theme>=0.12 ; extra == 'dev' + - pyepics>=3.4.2 ; extra == 'dev' + - pyright ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-asyncio ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - pytest-faulthandler ; extra == 'dev' + - pytest-forked ; extra == 'dev' + - pytest-rerunfailures ; extra == 'dev' + - pytest-timeout ; extra == 'dev' + - ruff ; extra == 'dev' + - sphinx-autobuild ; extra == 'dev' + - sphinx-autodoc2 ; extra == 'dev' + - sphinxcontrib-mermaid ; extra == 'dev' + - sphinx-copybutton ; extra == 'dev' + - sphinx-design ; extra == 'dev' + - tox-direct ; extra == 'dev' + - types-mock ; extra == 'dev' + - types-pyyaml ; extra == 'dev' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl name: packaging version: '25.0' @@ -1220,6 +1665,36 @@ packages: sha256: 1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0 requires_dist: - pytest ; extra == 'tests' +- pypi: https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl + name: pydantic + version: 2.11.5 + sha256: f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7 + requires_dist: + - annotated-types>=0.6.0 + - pydantic-core==2.33.2 + - typing-extensions>=4.12.2 + - typing-inspection>=0.4.0 + - email-validator>=2.0.0 ; extra == 'email' + - tzdata ; python_full_version >= '3.9' and sys_platform == 'win32' and extra == 'timezone' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: pydantic-core + version: 2.33.2 + sha256: 9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d + requires_dist: + - typing-extensions>=4.6.0,!=4.7.0 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/90/cb/c13d8a74419dde9590ed6fab293b516f68316ac87d06569b79b5f446d519/pydantic_numpy-8.0.1-py3-none-any.whl + name: pydantic-numpy + version: 8.0.1 + sha256: bf4cd84f4f864074197e9cfeafddca76bfbd1c2ef48f88be7322cc75838de4ae + requires_dist: + - compress-pickle[lz4] + - numpy>=2,<3 + - pydantic>=2.0,<3.0 + - 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/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl name: pygments version: 2.19.1 @@ -1325,6 +1800,15 @@ packages: purls: [] size: 282480 timestamp: 1740379431762 +- pypi: https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl + name: referencing + version: 0.36.2 + sha256: e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0 + requires_dist: + - attrs>=22.2.0 + - rpds-py>=0.7.0 + - typing-extensions>=4.4.0 ; python_full_version < '3.13' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl name: requests version: 2.32.3 @@ -1347,11 +1831,36 @@ packages: - pyright==1.1.394 ; extra == 'lint' - pytest>=8 ; extra == 'test' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/05/7c/8abb70f9017a231c6c961a8941403ed6557664c0913e1bf413cbdc039e75/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: rpds-py + version: 0.25.1 + sha256: a49e1d7a4978ed554f095430b89ecc23f42014a50ac385eb0c4d163ce213c325 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/04/9e/91fe10ea09e2383f0d0531cf3da765fa01b3b9918e85df92081813259cf4/ruamel.yaml-0.18.12-py3-none-any.whl + name: ruamel-yaml + version: 0.18.12 + sha256: 790ba4c48b6a6e6b12b532a7308779eb12d2aaab3a80fdb8389216f28ea2b287 + requires_dist: + - ruamel-yaml-clib>=0.2.7 ; python_full_version < '3.14' and platform_python_implementation == 'CPython' + - ruamel-yaml-jinja2>=0.2 ; extra == 'jinja2' + - ryd ; extra == 'docs' + - mercurial>5.7 ; extra == 'docs' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/1f/8f/ecfbe2123ade605c49ef769788f79c38ddb1c8fa81e01f4dbf5cf1a44b16/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: ruamel-yaml-clib + version: 0.2.12 + sha256: 0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/0e/2c/1e364cc92970075d7d04c69c928430b23e43a433f044474f57e425cbed37/ruff-0.11.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl name: ruff version: 0.11.7 sha256: 7940665e74e7b65d427b82bffc1e46710ec7f30d58b4b2d5016e3f0321436502 requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl + name: semver + version: 3.0.4 + sha256: 9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746 + requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl name: six version: 1.17.0 @@ -1541,6 +2050,27 @@ packages: purls: [] size: 3318875 timestamp: 1699202167581 +- pypi: https://files.pythonhosted.org/packages/03/98/eb27cc78ad3af8e302c9d8ff4977f5026676e130d28dd7578132a457170c/toolz-1.0.0-py3-none-any.whl + name: toolz + version: 1.0.0 + sha256: 292c8f1c4e7516bf9086f8850935c799a874039c8bcf959d47b600e4c44a6236 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl + name: tqdm + version: 4.67.1 + sha256: 26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2 + requires_dist: + - colorama ; sys_platform == 'win32' + - pytest>=6 ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - pytest-timeout ; extra == 'dev' + - pytest-asyncio>=0.24 ; extra == 'dev' + - nbval ; extra == 'dev' + - requests ; extra == 'discord' + - slack-sdk ; extra == 'slack' + - requests ; extra == 'telegram' + - ipywidgets>=6 ; extra == 'notebook' + requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl name: traitlets version: 5.14.3 @@ -1561,6 +2091,13 @@ packages: version: 4.13.2 sha256: a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl + name: typing-inspection + version: 0.4.1 + sha256: 389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51 + requires_dist: + - typing-extensions>=4.12.0 + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl name: tzdata version: '2025.2' diff --git a/pyproject.toml b/pyproject.toml index d9a1fc14..2990ab99 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ classifiers = [ dynamic = ["version"] dependencies = [ "ophyd", + "ophyd-async >=0.10.0a4", "h5py", ] diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py new file mode 100644 index 00000000..85934366 --- /dev/null +++ b/src/cditools/eiger_async.py @@ -0,0 +1,68 @@ +""" +Ophyd Async implementation for Eiger detector. +""" +from typing import Sequence, Annotated as A + +from ophyd_async.core import PathProvider, TriggerInfo, SignalRW, SignalR, DetectorTrigger +from ophyd_async.epics.signal import PvSuffix +from ophyd_async.epics.adcore import AreaDetector, ADWriter, NDPluginBaseIO, ADBaseController, ADBaseIO + + +class EigerTriggerInfo(TriggerInfo): + photon_energy: float + + +class EigerWriter(ADWriter): + pass + + +class EigerDriverIO(ADBaseIO): + photon_energy: A[SignalRW[float], PvSuffix.rbv("PhotonEnergy")] + trigger_mode: A[SignalRW[str], PvSuffix.rbv("TriggerMode")] + + +class EigerController(ADBaseController[EigerDriverIO]): + def __init__(self, driver: EigerDriverIO): + self._driver = driver + super().__init__(driver) + + async def prepare(self, trigger_info: EigerTriggerInfo): + await self._driver.photon_energy.set(trigger_info.photon_energy) + + if trigger_info.trigger != DetectorTrigger.INTERNAL: + raise NotImplementedError("Only internal trigger is supported") + else: + await self._driver.trigger_mode.set("ints") + + await super().prepare(trigger_info) + + +class EigerDetector(AreaDetector[EigerController]): + + def __init__( + self, + prefix: str, + path_provider: PathProvider, + driver_suffix: str = "cam1:", + writer_cls: type[ADWriter] = EigerWriter, + fileio_suffix: str = "cam1:", + name: str = "", + config_sigs: Sequence[SignalR] = (), + plugins: dict[str, NDPluginBaseIO] = {}, + ): + driver = EigerDriverIO(prefix + driver_suffix) + controller = EigerController(driver) + writer = writer_cls.with_io( + prefix, + path_provider, + dataset_source=driver, + fileio_suffix=fileio_suffix, + plugins=plugins, + ) + super().__init__( + controller=controller, + writer=writer, + plugins=plugins, + name=name, + config_sigs=config_sigs, + ) From cec0d987e32b94eabae8fa7167609faf916ecced Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Wed, 11 Jun 2025 17:25:40 -0400 Subject: [PATCH 02/46] Draft of Eiger async --- .gitignore | 4 + src/cditools/eiger_async.py | 401 +++++++++++++++++++++++++++++++++--- 2 files changed, 379 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index 75e5e330..cc380dd8 100644 --- a/.gitignore +++ b/.gitignore @@ -160,3 +160,7 @@ Thumbs.db # pixi environments .pixi *.egg-info + +# IDE files +.vscode/ +.cursor/ diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index 85934366..e0c16072 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -1,11 +1,229 @@ """ Ophyd Async implementation for Eiger detector. """ +import asyncio +import uuid +import time from typing import Sequence, Annotated as A +from pathlib import Path -from ophyd_async.core import PathProvider, TriggerInfo, SignalRW, SignalR, DetectorTrigger +from ophyd_async.core import ( + PathProvider, TriggerInfo, SignalRW, SignalR, DetectorTrigger, + StandardDetector, Device, DatasetDescriber, DetectorController +) from ophyd_async.epics.signal import PvSuffix -from ophyd_async.epics.adcore import AreaDetector, ADWriter, NDPluginBaseIO, ADBaseController, ADBaseIO +from ophyd_async.epics.adcore import ADWriter, NDPluginBaseIO, ADBaseIO, AreaDetector, ADBaseController, ADBaseDatasetDescriber +from ophyd_async.core import StrictEnum + + +class EigerTriggerMode(StrictEnum): + """Trigger modes for the Eiger detector. + + See https://areadetector.github.io/areaDetector/ADEiger/eiger.html#implementation-of-standard-driver-parameters + """ + INTERNAL_SERIES = "ints" + INTERNAL_ENABLE = "int" + EXTERNAL_SERIES = "exts" + EXTERNAL_ENABLE = "ext" + EXTERNAL_GATE = "gate" + + +class EigerExtGateMode(StrictEnum): + """External gate modes for the Eiger detector. + + See https://areadetector.github.io/areaDetector/ADEiger/eiger.html#trigger-setup + """ + PUMP_AND_PROBE = "Pump & Probe" + HDR = "HDR" + + +class EigerROIMode(StrictEnum): + """ROI modes for the Eiger detector. + + See https://areadetector.github.io/areaDetector/ADEiger/eiger.html#readout-setup + """ + DISABLED = "Disabled" + _4M = "4M" + + +class EigerCompressionAlgo(StrictEnum): + """Compression algorithms for the Eiger detector. + + See https://areadetector.github.io/areaDetector/ADEiger/eiger.html#readout-setup + """ + LZ4 = "lz4" + BSLZ4 = "bslz4" + NONE = "None" + + +class EigerDataSource(StrictEnum): + """Data sources for the Eiger detector. + + See https://areadetector.github.io/areaDetector/ADEiger/eiger.html#readout-setup + """ + NONE = "None" + FILE_WRITER = "FileWriter" + STREAM = "Stream" + + +class EigerHDF5Format(StrictEnum): + """HDF5 formats for the Eiger detector. + + See https://areadetector.github.io/areaDetector/ADEiger/eiger.html#filewriter-interface + """ + LEGACY = "Legacy" + V2024_2 = "v2024.2" + + +class EigerStreamVersion(StrictEnum): + """Stream versions for the Eiger detector. + + See https://areadetector.github.io/areaDetector/ADEiger/eiger.html#stream-interface + """ + STREAM1 = "Stream1" + STREAM2 = "Stream2" + + +class EigerStreamHdrDetail(StrictEnum): + """Header detail levels for the Eiger detector. + + See https://areadetector.github.io/areaDetector/ADEiger/eiger.html#stream-interface + """ + ALL = "All" + BASIC = "Basic" + NONE = "None" + + +class EigerDriverIO(ADBaseIO): + """Defines the full specifics of the Eiger driver. + + See https://areadetector.github.io/areaDetector/ADEiger/eiger.html#implementation-of-standard-driver-parameters + """ + # Standard Driver Parameters + trigger_mode: A[SignalRW[EigerTriggerMode], PvSuffix.rbv("TriggerMode")] + num_images: A[SignalRW[int], PvSuffix.rbv("NumImages")] + num_exposures: A[SignalRW[int], PvSuffix.rbv("NumExposures")] + acquire_time: A[SignalRW[float], PvSuffix.rbv("AcquireTime")] + acquire_period: A[SignalRW[float], PvSuffix.rbv("AcquirePeriod")] + data_type: A[SignalRW[str], PvSuffix.rbv("DataType")] + temperature_actual: A[SignalRW[float], PvSuffix.rbv("TemperatureActual")] + max_size_x: A[SignalRW[int], PvSuffix.rbv("MaxSizeX")] + max_size_y: A[SignalRW[int], PvSuffix.rbv("MaxSizeY")] + array_size_x: A[SignalRW[int], PvSuffix.rbv("ArraySizeX")] + array_size_y: A[SignalRW[int], PvSuffix.rbv("ArraySizeY")] + manufacturer: A[SignalRW[str], PvSuffix.rbv("Manufacturer")] + model: A[SignalRW[str], PvSuffix.rbv("Model")] + serial_number: A[SignalRW[str], PvSuffix.rbv("SerialNumber")] + firmware_version: A[SignalRW[str], PvSuffix.rbv("FirmwareVersion")] + sdk_version: A[SignalRW[str], PvSuffix.rbv("SDKVersion")] + driver_version: A[SignalRW[str], PvSuffix.rbv("DriverVersion")] + + # Detector Information + description: A[SignalR[str], PvSuffix("Description_RBV")] + x_pixel_size: A[SignalR[float], PvSuffix("XPixelSize_RBV")] + y_pixel_size: A[SignalR[float], PvSuffix("YPixelSize_RBV")] + sensor_material: A[SignalR[str], PvSuffix("SensorMaterial_RBV")] + sensor_thickness: A[SignalR[float], PvSuffix("SensorThickness_RBV")] + dead_time: A[SignalR[float], PvSuffix("DeadTime_RBV")] + + # Detector Status + state: A[SignalR[str], PvSuffix("State_RBV")] + error: A[SignalR[str], PvSuffix("Error_RBV")] + temp0: A[SignalR[float], PvSuffix("Temp0_RBV")] + humid0: A[SignalR[float], PvSuffix("Humid0_RBV")] + link0: A[SignalR[str], PvSuffix("Link0_RBV")] + link1: A[SignalR[str], PvSuffix("Link1_RBV")] + link2: A[SignalR[str], PvSuffix("Link2_RBV")] + link3: A[SignalR[str], PvSuffix("Link3_RBV")] + dcu_buffer_free: A[SignalR[int], PvSuffix("DCUBufferFree_RBV")] + hv_reset_time: A[SignalRW[float], PvSuffix.rbv("HVResetTime")] + hv_reset: A[SignalRW[bool], PvSuffix("HVReset", "HVReset")] + hv_state: A[SignalR[str], PvSuffix("HVState_RBV")] + + # Acquisition Setup + threshold: A[SignalRW[float], PvSuffix.rbv("Threshold")] + threshold1_enable: A[SignalRW[bool], PvSuffix.rbv("Threshold1Enable")] + threshold2: A[SignalRW[float], PvSuffix.rbv("Threshold2")] + threshold2_enable: A[SignalRW[bool], PvSuffix.rbv("Threshold2Enable")] + threshold_diff_enable: A[SignalRW[bool], PvSuffix.rbv("ThresholdDiffEnable")] + photon_energy: A[SignalRW[float], PvSuffix.rbv("PhotonEnergy")] + counting_mode: A[SignalRW[str], PvSuffix.rbv("CountingMode")] + + # Trigger Setup + ext_gate_mode: A[SignalRW[str], PvSuffix.rbv("ExtGateMode")] + trigger: A[SignalRW[float], PvSuffix("Trigger")] + trigger_exposure: A[SignalRW[float], PvSuffix.rbv("TriggerExposure")] + num_triggers: A[SignalRW[float], PvSuffix.rbv("NumTriggers")] + manual_trigger: A[SignalRW[bool], PvSuffix.rbv("ManualTrigger")] + trigger_start_delay: A[SignalRW[float], PvSuffix.rbv("TriggerStartDelay")] + + # Readout Setup + roi_mode: A[SignalRW[EigerROIMode], PvSuffix.rbv("ROIMode")] + flatfield_applied: A[SignalRW[bool], PvSuffix.rbv("FlatfieldApplied")] + countrate_corr_applied: A[SignalRW[bool], PvSuffix.rbv("CountrateCorrApplied")] + pixel_mask_applied: A[SignalRW[bool], PvSuffix.rbv("PixelMaskApplied")] + auto_summation: A[SignalRW[bool], PvSuffix.rbv("AutoSummation")] + signed_data: A[SignalRW[bool], PvSuffix.rbv("SignedData")] + compression_algo: A[SignalRW[EigerCompressionAlgo], PvSuffix.rbv("CompressionAlgo")] + data_source: A[SignalRW[EigerDataSource], PvSuffix.rbv("DataSource")] + + # Acquisition Status + armed: A[SignalR[bool], PvSuffix("Armed")] + bit_depth_image: A[SignalR[int], PvSuffix("BitDepthImage_RBV")] + count_cutoff: A[SignalR[float], PvSuffix("CountCutoff_RBV")] + + # FileWriter Interface + fw_enable: A[SignalRW[bool], PvSuffix.rbv("FWEnable")] + fw_state: A[SignalR[str], PvSuffix("FWState_RBV")] + fw_hdf5_format: A[SignalRW[EigerHDF5Format], PvSuffix.rbv("FWHDF5Format")] + fw_compression: A[SignalRW[bool], PvSuffix.rbv("FWCompression")] + fw_nimgs_per_file: A[SignalRW[float], PvSuffix.rbv("FWNImgsPerFile")] + fw_name_pattern: A[SignalRW[str], PvSuffix.rbv("FWNamePattern")] + sequence_id: A[SignalR[float], PvSuffix("SequenceId")] + save_files: A[SignalRW[bool], PvSuffix.rbv("SaveFiles")] + file_owner: A[SignalRW[str], PvSuffix.rbv("FileOwner")] + file_owner_grp: A[SignalRW[str], PvSuffix.rbv("FileOwnerGrp")] + file_perms: A[SignalRW[str], PvSuffix.rbv("FilePerms")] + fw_free: A[SignalR[float], PvSuffix("FWFree_RBV")] + fw_auto_remove: A[SignalRW[bool], PvSuffix.rbv("FWAutoRemove")] + fw_clear: A[SignalRW[float], PvSuffix("FWClear")] + + # Stream Interface + stream_enable: A[SignalRW[bool], PvSuffix.rbv("StreamEnable")] + stream_state: A[SignalR[str], PvSuffix("StreamState_RBV")] + stream_version: A[SignalRW[EigerStreamVersion], PvSuffix.rbv("StreamVersion")] + stream_decompress: A[SignalRW[bool], PvSuffix.rbv("StreamDecompress")] + stream_hdr_detail: A[SignalRW[EigerStreamHdrDetail], PvSuffix.rbv("StreamHdrDetail")] + stream_hdr_appendix: A[SignalRW[str], PvSuffix.rbv("StreamHdrAppendix")] + stream_img_appendix: A[SignalRW[str], PvSuffix.rbv("StreamImgAppendix")] + stream_dropped: A[SignalR[float], PvSuffix("StreamDropped_RBV")] + + # Monitor Interface + monitor_enable: A[SignalRW[bool], PvSuffix.rbv("MonitorEnable")] + monitor_state: A[SignalR[str], PvSuffix("MonitorState_RBV")] + monitor_timeout: A[SignalRW[float], PvSuffix.rbv("MonitorTimeout")] + + # Acquisition Metadata + beam_x: A[SignalRW[float], PvSuffix.rbv("BeamX")] + beam_y: A[SignalRW[float], PvSuffix.rbv("BeamY")] + det_dist: A[SignalRW[float], PvSuffix.rbv("DetDist")] + wavelength: A[SignalRW[float], PvSuffix.rbv("Wavelength")] + + # Detector Metadata + chi_start: A[SignalRW[float], PvSuffix.rbv("ChiStart")] + chi_incr: A[SignalRW[float], PvSuffix.rbv("ChiIncr")] + kappa_start: A[SignalRW[float], PvSuffix.rbv("KappaStart")] + kappa_incr: A[SignalRW[float], PvSuffix.rbv("KappaIncr")] + omega_start: A[SignalRW[float], PvSuffix.rbv("OmegaStart")] + omega_incr: A[SignalRW[float], PvSuffix.rbv("OmegaIncr")] + phi_start: A[SignalRW[float], PvSuffix.rbv("PhiStart")] + phi_incr: A[SignalRW[float], PvSuffix.rbv("PhiIncr")] + two_theta_start: A[SignalRW[float], PvSuffix.rbv("TwoThetaStart")] + two_theta_incr: A[SignalRW[float], PvSuffix.rbv("TwoThetaIncr")] + + # Minimum change allowed + wavelength_eps: A[SignalRW[float], PvSuffix.rbv("WavelengthEps")] + energy_eps: A[SignalRW[float], PvSuffix.rbv("EnergyEps")] class EigerTriggerInfo(TriggerInfo): @@ -13,56 +231,187 @@ class EigerTriggerInfo(TriggerInfo): class EigerWriter(ADWriter): - pass + """Eiger-specific file writer using the built-in FileWriter interface.""" + + def __init__(self, dataset_source: EigerDriverIO, path_provider: PathProvider): + dataset_describer = ADBaseDatasetDescriber(dataset_source) + + # Initialize with empty fileio since Eiger handles its own file writing + super().__init__( + fileio=Device(""), # TODO: Dummy device since Eiger handles file writing internally + path_provider=path_provider, + dataset_describer=dataset_describer, + ) + self._dataset_source = dataset_source + self._file_info = None + self._num_captured = 0 + async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, any]: + """Setup file writing for acquisition.""" + # Get file path info from path provider + self._file_info = self._path_provider() + file_path = Path(self._file_info.directory) / self._file_info.filename + + # Configure the Eiger FileWriter + await self._dataset_source.fw_enable.set(True) + await self._dataset_source.save_files.set(True) + await self._dataset_source.fw_name_pattern.set(str(file_path.with_suffix(".h5"))) + + # Set HDF5 format and compression + await self._dataset_source.fw_hdf5_format.set(EigerHDF5Format.V2024_2) + await self._dataset_source.fw_compression.set(True) + + # Enable data source for file writer + await self._dataset_source.data_source.set(EigerDataSource.FILE_WRITER) + + # Get dataset info for the describe output + shape = await self._dataset_describer.shape() + dtype = await self._dataset_describer.np_datatype() + + return { + name: { + "source": str(file_path.with_suffix(".h5")), + "shape": shape, + "dtype": dtype, + "external": "FILESTORE:" + str(file_path.with_suffix(".h5")), + } + } -class EigerDriverIO(ADBaseIO): - photon_energy: A[SignalRW[float], PvSuffix.rbv("PhotonEnergy")] - trigger_mode: A[SignalRW[str], PvSuffix.rbv("TriggerMode")] + async def observe_indices_written(self, timeout: float = 10.0): + """Monitor the number of files written by the Eiger FileWriter.""" + last_count = 0 + while True: + try: + # Check sequence ID to see how many images have been written + current_count = await self._dataset_source.sequence_id.get_value() + if current_count > last_count: + for i in range(last_count, int(current_count)): + yield i + last_count = int(current_count) + await asyncio.sleep(0.1) + except asyncio.TimeoutError: + break + + async def get_indices_written(self) -> int: + """Get the current number of indices written.""" + sequence_id = await self._dataset_source.sequence_id.get_value() + return int(sequence_id) + + async def collect_stream_docs(self, name: str, indices_written: int): + """Generate stream documents for the written HDF5 files.""" + if self._file_info is None: + return + + # For Eiger, we typically get one HDF5 file per acquisition + file_path = Path(self._file_info.directory) / f"{self._file_info.filename}.h5" + + # Create stream resource document + stream_resource = { + "spec": "EIGER_HDF5", + "root": str(self._file_info.directory), + "resource_path": str(file_path.name), + "resource_kwargs": {}, + "path_semantics": "posix", + "uid": str(uuid.uuid4()), + } + + yield "stream_resource", stream_resource + + # Create stream datum documents for each frame + for i in range(indices_written): + stream_datum = { + "descriptor": name, + "stream_resource": stream_resource["uid"], + "seq_num": i, + "timestamps": [time.time()], + } + yield "stream_datum", stream_datum + + async def close(self) -> None: + """Clean up file writing after acquisition.""" + # Disable file writer + await self._dataset_source.fw_enable.set(False) + await self._dataset_source.save_files.set(False) class EigerController(ADBaseController[EigerDriverIO]): + """Controller for Eiger detector, handling trigger modes and acquisition setup.""" + def __init__(self, driver: EigerDriverIO): - self._driver = driver super().__init__(driver) - async def prepare(self, trigger_info: EigerTriggerInfo): - await self._driver.photon_energy.set(trigger_info.photon_energy) + def get_deadtime(self, exposure: float | None) -> float: + """Get detector deadtime for the given exposure. + + For Eiger, deadtime is typically constant and available from the driver. + """ + # Use the deadtime from the detector, or a default if exposure is None + return 0.001 # 1ms typical deadtime for Eiger, can be read from driver.dead_time - if trigger_info.trigger != DetectorTrigger.INTERNAL: - raise NotImplementedError("Only internal trigger is supported") + async def prepare(self, trigger_info: EigerTriggerInfo) -> None: + """Prepare the detector for acquisition.""" + # Set photon energy + await self._driver.photon_energy.set(trigger_info.photon_energy) + + # Configure trigger mode based on TriggerInfo + if trigger_info.trigger == DetectorTrigger.INTERNAL: + await self._driver.trigger_mode.set(EigerTriggerMode.INTERNAL_SERIES) + elif trigger_info.trigger == DetectorTrigger.EXTERNAL: + await self._driver.trigger_mode.set(EigerTriggerMode.EXTERNAL_SERIES) else: - await self._driver.trigger_mode.set("ints") - + raise NotImplementedError(f"Trigger mode {trigger_info.trigger} not supported") + + # Call parent prepare method to handle standard areaDetector setup await super().prepare(trigger_info) + async def arm(self) -> None: + """Arm the detector for acquisition.""" + # Use parent implementation which handles standard areaDetector arming + await super().arm() + + async def wait_for_idle(self) -> None: + """Wait for detector to become idle.""" + # Use parent implementation + await super().wait_for_idle() + + async def disarm(self) -> None: + """Disarm the detector after acquisition.""" + # Use parent implementation + await super().disarm() + class EigerDetector(AreaDetector[EigerController]): + """Eiger detector implementation using AreaDetector pattern.""" def __init__( self, prefix: str, path_provider: PathProvider, driver_suffix: str = "cam1:", - writer_cls: type[ADWriter] = EigerWriter, - fileio_suffix: str = "cam1:", name: str = "", config_sigs: Sequence[SignalR] = (), - plugins: dict[str, NDPluginBaseIO] = {}, + plugins: dict[str, NDPluginBaseIO] | None = None, ): - driver = EigerDriverIO(prefix + driver_suffix) + # Create driver IO + driver = EigerDriverIO(prefix + driver_suffix, name="driver") + + # Create controller controller = EigerController(driver) - writer = writer_cls.with_io( - prefix, - path_provider, - dataset_source=driver, - fileio_suffix=fileio_suffix, - plugins=plugins, - ) + + # Create writer + writer = EigerWriter(dataset_source=driver, path_provider=path_provider) + super().__init__( controller=controller, writer=writer, - plugins=plugins, - name=name, + plugins=plugins or {}, config_sigs=config_sigs, + name=name, ) + + # Store driver for external access + self.driver = driver + + async def prepare(self, value: EigerTriggerInfo) -> None: + """Prepare detector with Eiger-specific trigger info.""" + await super().prepare(value) From 9e1953258d8b9e11cd038082608b3fff2ef6e332 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Mon, 30 Jun 2025 11:54:27 -0400 Subject: [PATCH 03/46] Added proper file writing and simplified implementation --- pixi.lock | 122 ++++++++++++++- pyproject.toml | 3 + src/cditools/eiger_async.py | 295 ++++++++++++++++++++++-------------- tests/conftest.py | 75 +++++++++ tests/test_eiger_async.py | 276 +++++++++++++++++++++++++++++++++ 5 files changed, 656 insertions(+), 115 deletions(-) create mode 100644 tests/conftest.py create mode 100644 tests/test_eiger_async.py diff --git a/pixi.lock b/pixi.lock index 5b87dec4..692de071 100644 --- a/pixi.lock +++ b/pixi.lock @@ -101,6 +101,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - pypi: https://files.pythonhosted.org/packages/5b/08/185c3b29b0698328b202e6c965c23187e2e29ead78cb468aab0a09ee97fc/aioca-1.8.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl @@ -115,6 +116,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/11/79/479e2194c9096b92aecdf33634ae948d2be306c6011673e98ee1917f32c2/dpkt-1.9.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4a/18/3853de30cc92c164eccff6e62599ef88d0aa3b64740973b63e13f88145de/epicscorelibs-7.0.7.99.1.1.tar.gz - pypi: https://files.pythonhosted.org/packages/b1/f2/47e3f0e06c3bc3834db2a4ed01641e1ce493108461b9540dd5d2fb60d334/event_model-1.22.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl @@ -158,6 +160,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/90/cb/c13d8a74419dde9590ed6fab293b516f68316ac87d06569b79b5f446d519/pydantic_numpy-8.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/30/05/ce271016e351fddc8399e546f6e23761967ee09c8c568bbfbecb0c150171/pytest_asyncio-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl @@ -168,6 +171,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/1f/8f/ecfbe2123ade605c49ef769788f79c38ddb1c8fa81e01f4dbf5cf1a44b16/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/0e/2c/1e364cc92970075d7d04c69c928430b23e43a433f044474f57e425cbed37/ruff-0.11.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b9/36/c101788fad13e8ea65c5b3d3dee8ff996500800cd554ae6ff72143690247/setuptools_dso-2.12.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/03/98/eb27cc78ad3af8e302c9d8ff4977f5026676e130d28dd7578132a457170c/toolz-1.0.0-py3-none-any.whl @@ -350,6 +355,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.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/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/30/05/ce271016e351fddc8399e546f6e23761967ee09c8c568bbfbecb0c150171/pytest_asyncio-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl @@ -386,6 +392,30 @@ packages: purls: [] size: 23621 timestamp: 1650670423406 +- pypi: https://files.pythonhosted.org/packages/5b/08/185c3b29b0698328b202e6c965c23187e2e29ead78cb468aab0a09ee97fc/aioca-1.8.1-py3-none-any.whl + name: aioca + version: 1.8.1 + sha256: b856b68c4722387bc88917c10f71f87b7e74d95a28581187b6577682a6c68909 + requires_dist: + - numpy + - epicscorelibs>=7.0.3.99.4.0 + - black ; extra == 'dev' + - click ; extra == 'dev' + - mypy ; extra == 'dev' + - myst-parser ; extra == 'dev' + - pipdeptree ; extra == 'dev' + - pre-commit ; extra == 'dev' + - pydata-sphinx-theme>=0.12 ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-asyncio ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - ruff ; extra == 'dev' + - sphinx-autobuild ; extra == 'dev' + - sphinx-copybutton ; extra == 'dev' + - sphinx-design ; extra == 'dev' + - tox-direct ; extra == 'dev' + - types-mock ; extra == 'dev' + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl name: alabaster version: 1.0.0 @@ -622,22 +652,25 @@ packages: requires_python: '>=3.8' - pypi: . name: cditools - version: 0.1.dev23+g6e8b9de.d20250603 - sha256: a283f4a7570f79bc9f22553e6cb1ffc12ec0385473c900d81185dbc46516f800 + version: 0.1.dev25+gcec0d98.d20250630 + sha256: 786ea3d4db672c2c44cf9dd24352a746e6d16ab8e8c1541eb9ec9c027abd77cc requires_dist: - ophyd - ophyd-async>=0.10.0a4 - h5py - pytest>=6 ; extra == 'test' - pytest-cov>=3 ; extra == 'test' + - pytest-asyncio>=0.18 ; extra == 'test' - caproto[standard]>=0.4.2rc1,!=1.2.0 ; extra == 'test' - caproto[standard]>=0.4.2rc1,!=1.2.0 ; extra == 'dev' - pytest>=6 ; extra == 'dev' - pytest-cov>=3 ; extra == 'dev' + - pytest-asyncio>=0.18 ; extra == 'dev' - pandas ; extra == 'dev' - pre-commit ; extra == 'dev' - ipython ; extra == 'dev' - ruff ; extra == 'dev' + - aioca ; extra == 'dev' - sphinx>=7.0 ; extra == 'docs' - myst-parser>=0.13 ; extra == 'docs' - sphinx-copybutton ; extra == 'docs' @@ -734,6 +767,15 @@ packages: name: dpkt version: 1.9.8 sha256: 4da4d111d7bf67575b571f5c678c71bddd2d8a01a3d57d489faf0a92c748fbfd +- pypi: https://files.pythonhosted.org/packages/4a/18/3853de30cc92c164eccff6e62599ef88d0aa3b64740973b63e13f88145de/epicscorelibs-7.0.7.99.1.1.tar.gz + name: epicscorelibs + version: 7.0.7.99.1.1 + sha256: f9dd2c01913cf13959e882c7c2afccd5d55cd5084bd4597201ea119f65752231 + requires_dist: + - setuptools + - setuptools-dso>=2.11a2 + - numpy + requires_python: '>=2.7' - pypi: https://files.pythonhosted.org/packages/b1/f2/47e3f0e06c3bc3834db2a4ed01641e1ce493108461b9540dd5d2fb60d334/event_model-1.22.3-py3-none-any.whl name: event-model version: 1.22.3 @@ -1722,6 +1764,18 @@ packages: - setuptools ; extra == 'dev' - xmlschema ; extra == 'dev' requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/30/05/ce271016e351fddc8399e546f6e23761967ee09c8c568bbfbecb0c150171/pytest_asyncio-1.0.0-py3-none-any.whl + name: pytest-asyncio + version: 1.0.0 + sha256: 4f024da9f1ef945e680dc68610b52550e36590a67fd31bb3b4943979a1f90ef3 + requires_dist: + - pytest>=8.2,<9 + - typing-extensions>=4.12 ; python_full_version < '3.10' + - sphinx>=5.3 ; extra == 'docs' + - sphinx-rtd-theme>=1 ; extra == 'docs' + - coverage>=6.2 ; extra == 'testing' + - hypothesis>=5.7.1 ; extra == 'testing' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl name: pytest-cov version: 6.1.1 @@ -1861,6 +1915,70 @@ packages: version: 3.0.4 sha256: 9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746 requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + name: setuptools + version: 80.9.0 + sha256: 062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 + requires_dist: + - pytest>=6,!=8.1.* ; extra == 'test' + - virtualenv>=13.0.0 ; extra == 'test' + - wheel>=0.44.0 ; extra == 'test' + - pip>=19.1 ; extra == 'test' + - packaging>=24.2 ; extra == 'test' + - jaraco-envs>=2.2 ; extra == 'test' + - pytest-xdist>=3 ; extra == 'test' + - jaraco-path>=3.7.2 ; extra == 'test' + - build[virtualenv]>=1.0.3 ; extra == 'test' + - filelock>=3.4.0 ; extra == 'test' + - ini2toml[lite]>=0.14 ; extra == 'test' + - tomli-w>=1.0.0 ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest-perf ; sys_platform != 'cygwin' and extra == 'test' + - jaraco-develop>=7.21 ; python_full_version >= '3.9' and sys_platform != 'cygwin' and extra == 'test' + - pytest-home>=0.5 ; extra == 'test' + - pytest-subprocess ; extra == 'test' + - pyproject-hooks!=1.1 ; extra == 'test' + - jaraco-test>=5.5 ; extra == 'test' + - sphinx>=3.5 ; extra == 'doc' + - jaraco-packaging>=9.3 ; extra == 'doc' + - rst-linker>=1.9 ; extra == 'doc' + - furo ; extra == 'doc' + - sphinx-lint ; extra == 'doc' + - jaraco-tidelift>=1.4 ; extra == 'doc' + - pygments-github-lexers==0.0.5 ; extra == 'doc' + - sphinx-favicon ; extra == 'doc' + - sphinx-inline-tabs ; extra == 'doc' + - sphinx-reredirects ; extra == 'doc' + - sphinxcontrib-towncrier ; extra == 'doc' + - sphinx-notfound-page>=1,<2 ; extra == 'doc' + - pyproject-hooks!=1.1 ; extra == 'doc' + - towncrier<24.7 ; extra == 'doc' + - packaging>=24.2 ; extra == 'core' + - more-itertools>=8.8 ; extra == 'core' + - jaraco-text>=3.7 ; extra == 'core' + - importlib-metadata>=6 ; python_full_version < '3.10' and extra == 'core' + - tomli>=2.0.1 ; python_full_version < '3.11' and extra == 'core' + - wheel>=0.43.0 ; extra == 'core' + - platformdirs>=4.2.2 ; extra == 'core' + - jaraco-functools>=4 ; extra == 'core' + - more-itertools ; extra == 'core' + - pytest-checkdocs>=2.4 ; extra == 'check' + - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' + - ruff>=0.8.0 ; sys_platform != 'cygwin' and extra == 'check' + - pytest-cov ; extra == 'cover' + - pytest-enabler>=2.2 ; extra == 'enabler' + - pytest-mypy ; extra == 'type' + - mypy==1.14.* ; extra == 'type' + - importlib-metadata>=7.0.2 ; python_full_version < '3.10' and extra == 'type' + - jaraco-develop>=7.21 ; sys_platform != 'cygwin' and extra == 'type' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/b9/36/c101788fad13e8ea65c5b3d3dee8ff996500800cd554ae6ff72143690247/setuptools_dso-2.12.2-py2.py3-none-any.whl + name: setuptools-dso + version: 2.12.2 + sha256: deb786b1cfac92f252a82df38e5129b1d9341b36885a01f64543bf3500734a21 + requires_dist: + - setuptools + requires_python: '>=2.7' - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl name: six version: 1.17.0 diff --git a/pyproject.toml b/pyproject.toml index 2990ab99..a075df7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,16 +39,19 @@ dependencies = [ test = [ "pytest >=6", "pytest-cov >=3", + "pytest-asyncio >=0.18", "caproto[standard] >=0.4.2rc1,!=1.2.0", ] dev = [ "caproto[standard] >=0.4.2rc1,!=1.2.0", "pytest >=6", "pytest-cov >=3", + "pytest-asyncio >=0.18", "pandas", "pre-commit", "ipython", "ruff", + "aioca", ] docs = [ "sphinx>=7.0", diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index e0c16072..4ca097bb 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -4,17 +4,21 @@ import asyncio import uuid import time -from typing import Sequence, Annotated as A +from typing import Sequence, Annotated as A, Any from pathlib import Path +from logging import getLogger +from datetime import datetime from ophyd_async.core import ( PathProvider, TriggerInfo, SignalRW, SignalR, DetectorTrigger, - StandardDetector, Device, DatasetDescriber, DetectorController + StandardDetector, Device, DatasetDescriber, DetectorController, DetectorWriter ) from ophyd_async.epics.signal import PvSuffix -from ophyd_async.epics.adcore import ADWriter, NDPluginBaseIO, ADBaseIO, AreaDetector, ADBaseController, ADBaseDatasetDescriber +from ophyd_async.epics.adcore import ADWriter, NDPluginBaseIO, ADBaseIO, AreaDetector, ADBaseController, ADBaseDatasetDescriber, ADImageMode, ADHDFWriter from ophyd_async.core import StrictEnum +logger = getLogger(__name__) + class EigerTriggerMode(StrictEnum): """Trigger modes for the Eiger detector. @@ -181,6 +185,7 @@ class EigerDriverIO(ADBaseIO): fw_name_pattern: A[SignalRW[str], PvSuffix.rbv("FWNamePattern")] sequence_id: A[SignalR[float], PvSuffix("SequenceId")] save_files: A[SignalRW[bool], PvSuffix.rbv("SaveFiles")] + file_path: A[SignalRW[str], PvSuffix.rbv("FilePath")] file_owner: A[SignalRW[str], PvSuffix.rbv("FileOwner")] file_owner_grp: A[SignalRW[str], PvSuffix.rbv("FileOwnerGrp")] file_perms: A[SignalRW[str], PvSuffix.rbv("FilePerms")] @@ -226,158 +231,216 @@ class EigerDriverIO(ADBaseIO): energy_eps: A[SignalRW[float], PvSuffix.rbv("EnergyEps")] -class EigerTriggerInfo(TriggerInfo): - photon_energy: float - - -class EigerWriter(ADWriter): - """Eiger-specific file writer using the built-in FileWriter interface.""" +class EigerWriter(DetectorWriter): + """Eiger-specific file writer using the built-in FileWriter interface. - def __init__(self, dataset_source: EigerDriverIO, path_provider: PathProvider): - dataset_describer = ADBaseDatasetDescriber(dataset_source) - - # Initialize with empty fileio since Eiger handles its own file writing - super().__init__( - fileio=Device(""), # TODO: Dummy device since Eiger handles file writing internally - path_provider=path_provider, - dataset_describer=dataset_describer, - ) - self._dataset_source = dataset_source - self._file_info = None - self._num_captured = 0 - - async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, any]: + Adapted from the original eiger.py EigerFileHandler implementation + to work with the ophyd-async DetectorWriter interface. + """ + + def __init__(self, driver: EigerDriverIO, path_provider: PathProvider): + self._path_provider = path_provider + self._driver = driver + self._sequence_id_offset = 1 + self._resource_uid = None + self._write_path = None + self._file_prefix = None + self._images_per_file = None + self._resource_kwargs = None + self._stream_resource_doc = None + self._initial_sequence_id = None + + async def open(self, multiplier: int = 1) -> dict[str, Any]: """Setup file writing for acquisition.""" # Get file path info from path provider self._file_info = self._path_provider() - file_path = Path(self._file_info.directory) / self._file_info.filename + + # Generate resource UID similar to original implementation + self._resource_uid = str(uuid.uuid4())[:8] # Similar to new_short_uid() + + # Create write path using current datetime + current_time = datetime.now() + self._write_path = Path(self._file_info.directory_path) / current_time.strftime("%Y/%m/%d") + + # Set up file path on the detector + await self._driver.file_path.set(str(self._write_path)) + + # Set the name pattern with $id replacement similar to original + name_pattern = f"{self._resource_uid}_$id" + await self._driver.fw_name_pattern.set(name_pattern) # Configure the Eiger FileWriter - await self._dataset_source.fw_enable.set(True) - await self._dataset_source.save_files.set(True) - await self._dataset_source.fw_name_pattern.set(str(file_path.with_suffix(".h5"))) + await asyncio.gather( + self._driver.fw_enable.set(True), + self._driver.save_files.set(True), + self._driver.fw_hdf5_format.set(EigerHDF5Format.V2024_2), + self._driver.fw_compression.set(True), + self._driver.data_source.set(EigerDataSource.FILE_WRITER), + ) + + # Get images per file for resource document + self._images_per_file = await self._driver.fw_nimgs_per_file.get_value() + + # Set the filename prefix for the resource document + self._file_prefix = self._write_path / self._resource_uid + + # Prepare resource kwargs similar to original implementation + self._resource_kwargs = {"images_per_file": int(self._images_per_file)} + + # Create the stream resource document + self._stream_resource_doc = { + "spec": "AD_EIGER", + "root": str(self._file_info.directory_path), + "resource_path": str(self._file_prefix.relative_to(self._file_info.directory_path)), + "resource_kwargs": self._resource_kwargs, + "path_semantics": "posix", + "uid": str(uuid.uuid4()), + } - # Set HDF5 format and compression - await self._dataset_source.fw_hdf5_format.set(EigerHDF5Format.V2024_2) - await self._dataset_source.fw_compression.set(True) + # Validate and create write path + if not self._write_path.exists(): + self._write_path.mkdir(parents=True, exist_ok=True) - # Enable data source for file writer - await self._dataset_source.data_source.set(EigerDataSource.FILE_WRITER) + # Get initial sequence ID to track changes + self._initial_sequence_id = await self._driver.sequence_id.get_value() # Get dataset info for the describe output - shape = await self._dataset_describer.shape() - dtype = await self._dataset_describer.np_datatype() + array_size_x = await self._driver.array_size_x.get_value() + array_size_y = await self._driver.array_size_y.get_value() + data_type = await self._driver.data_type.get_value() + + # Convert data type to numpy dtype + dtype_map = { + "UInt8": "uint8", + "UInt16": "uint16", + "UInt32": "uint32", + "Int8": "int8", + "Int16": "int16", + "Int32": "int32", + "Float32": "float32", + "Float64": "float64", + } + dtype = dtype_map.get(data_type, "uint16") # Default to uint16 return { - name: { - "source": str(file_path.with_suffix(".h5")), - "shape": shape, + "primary": { + "source": f"EIGER:{self._resource_uid}", + "shape": [int(array_size_y), int(array_size_x)], "dtype": dtype, - "external": "FILESTORE:" + str(file_path.with_suffix(".h5")), + "external": f"FILESTORE:{self._stream_resource_doc['uid']}", } } async def observe_indices_written(self, timeout: float = 10.0): """Monitor the number of files written by the Eiger FileWriter.""" - last_count = 0 + last_sequence_id = self._initial_sequence_id + while True: try: - # Check sequence ID to see how many images have been written - current_count = await self._dataset_source.sequence_id.get_value() - if current_count > last_count: - for i in range(last_count, int(current_count)): - yield i - last_count = int(current_count) + # Check sequence ID to see how many acquisitions have been written + current_sequence_id = await self._driver.sequence_id.get_value() + + if current_sequence_id > last_sequence_id: + # Yield indices for all new sequences since last check + for seq_id in range(int(last_sequence_id + 1), int(current_sequence_id + 1)): + # Convert sequence ID to 0-based index + index = seq_id - self._initial_sequence_id - 1 + yield index + last_sequence_id = current_sequence_id + await asyncio.sleep(0.1) except asyncio.TimeoutError: break async def get_indices_written(self) -> int: """Get the current number of indices written.""" - sequence_id = await self._dataset_source.sequence_id.get_value() - return int(sequence_id) + current_sequence_id = await self._driver.sequence_id.get_value() + return int(current_sequence_id - self._initial_sequence_id) - async def collect_stream_docs(self, name: str, indices_written: int): - """Generate stream documents for the written HDF5 files.""" - if self._file_info is None: + async def collect_stream_docs(self, indices_written: int): + """Generate stream documents for the written HDF5 files. + + Follows the pattern from the original EigerFileHandler.generate_datum method. + """ + if self._stream_resource_doc is None: return - # For Eiger, we typically get one HDF5 file per acquisition - file_path = Path(self._file_info.directory) / f"{self._file_info.filename}.h5" + # Yield the stream resource document first + yield "stream_resource", self._stream_resource_doc - # Create stream resource document - stream_resource = { - "spec": "EIGER_HDF5", - "root": str(self._file_info.directory), - "resource_path": str(file_path.name), - "resource_kwargs": {}, - "path_semantics": "posix", - "uid": str(uuid.uuid4()), - } - - yield "stream_resource", stream_resource - - # Create stream datum documents for each frame + # Generate stream datum documents for each index written for i in range(indices_written): + # Calculate the actual sequence number like in original implementation + sequence_number = self._sequence_id_offset + self._initial_sequence_id + i + stream_datum = { - "descriptor": name, - "stream_resource": stream_resource["uid"], + "descriptor": "primary", + "stream_resource": self._stream_resource_doc["uid"], "seq_num": i, "timestamps": [time.time()], + "datum_kwargs": {"seq_id": int(sequence_number)}, } yield "stream_datum", stream_datum async def close(self) -> None: - """Clean up file writing after acquisition.""" + """Clean up file writing after acquisition and validate files exist.""" # Disable file writer - await self._dataset_source.fw_enable.set(False) - await self._dataset_source.save_files.set(False) + await asyncio.gather( + self._driver.fw_enable.set(False), + self._driver.save_files.set(False), + ) + + # Validate that master files were written (similar to original unstage method) + if self._file_prefix is not None and self._initial_sequence_id is not None: + final_sequence_id = await self._driver.sequence_id.get_value() + indices_written = int(final_sequence_id - self._initial_sequence_id) + + # Check that master files exist for each sequence + missing_files = [] + for i in range(indices_written): + sequence_number = self._sequence_id_offset + self._initial_sequence_id + i + master_file = Path(f"{self._file_prefix}_{int(sequence_number)}_master.h5") + if not master_file.exists(): + missing_files.append(master_file) + + if missing_files: + logger.warning(f"Master files were not written: {missing_files}") class EigerController(ADBaseController[EigerDriverIO]): """Controller for Eiger detector, handling trigger modes and acquisition setup.""" - def __init__(self, driver: EigerDriverIO): - super().__init__(driver) + def __init__(self, driver: EigerDriverIO, *args: Any, **kwargs: dict[str, Any]) -> None: + super().__init__(driver, *args, **kwargs) def get_deadtime(self, exposure: float | None) -> float: - """Get detector deadtime for the given exposure. - - For Eiger, deadtime is typically constant and available from the driver. - """ - # Use the deadtime from the detector, or a default if exposure is None - return 0.001 # 1ms typical deadtime for Eiger, can be read from driver.dead_time + """Get detector deadtime for the given exposure.""" + return 0.001 - async def prepare(self, trigger_info: EigerTriggerInfo) -> None: + async def prepare(self, trigger_info: TriggerInfo) -> None: """Prepare the detector for acquisition.""" - # Set photon energy - await self._driver.photon_energy.set(trigger_info.photon_energy) - + if (exposure := trigger_info.livetime) is not None: + await self._driver.acquire_time.set(exposure) + # Configure trigger mode based on TriggerInfo if trigger_info.trigger == DetectorTrigger.INTERNAL: await self._driver.trigger_mode.set(EigerTriggerMode.INTERNAL_SERIES) elif trigger_info.trigger == DetectorTrigger.EXTERNAL: await self._driver.trigger_mode.set(EigerTriggerMode.EXTERNAL_SERIES) + elif trigger_info.trigger in [DetectorTrigger.VARIABLE_GATE, DetectorTrigger.CONSTANT_GATE]: + await self._driver.trigger_mode.set(EigerTriggerMode.EXTERNAL_GATE) else: raise NotImplementedError(f"Trigger mode {trigger_info.trigger} not supported") - - # Call parent prepare method to handle standard areaDetector setup - await super().prepare(trigger_info) - async def arm(self) -> None: - """Arm the detector for acquisition.""" - # Use parent implementation which handles standard areaDetector arming - await super().arm() - - async def wait_for_idle(self) -> None: - """Wait for detector to become idle.""" - # Use parent implementation - await super().wait_for_idle() + if trigger_info.total_number_of_exposures == 0: + image_mode = ADImageMode.CONTINUOUS + else: + image_mode = ADImageMode.MULTIPLE - async def disarm(self) -> None: - """Disarm the detector after acquisition.""" - # Use parent implementation - await super().disarm() + await asyncio.gather( + self._driver.num_images.set(trigger_info.total_number_of_exposures), + self._driver.image_mode.set(image_mode), + ) class EigerDetector(AreaDetector[EigerController]): @@ -388,30 +451,36 @@ def __init__( prefix: str, path_provider: PathProvider, driver_suffix: str = "cam1:", + writer_cls: type[DetectorWriter] = EigerWriter, + fileio_suffix: str | None = None, name: str = "", config_sigs: Sequence[SignalR] = (), plugins: dict[str, NDPluginBaseIO] | None = None, ): - # Create driver IO - driver = EigerDriverIO(prefix + driver_suffix, name="driver") - - # Create controller + driver = EigerDriverIO(prefix + driver_suffix) controller = EigerController(driver) - # Create writer - writer = EigerWriter(dataset_source=driver, path_provider=path_provider) + # If the writer class is an ADWriter, use the with_io method + # since we want to use one of the AD plugins. + # Otherwise, use the internal file writer. + if issubclass(writer_cls, ADWriter): + writer = writer_cls.with_io( + prefix, + path_provider, + dataset_source=driver, + fileio_suffix=fileio_suffix, + plugins=plugins, + ) + else: + if fileio_suffix is not None or plugins is not None: + logger.warning("Ignoring params fileio_suffix and plugins for non-ADWriter writer") + writer = writer_cls(driver, path_provider) super().__init__( controller=controller, writer=writer, - plugins=plugins or {}, - config_sigs=config_sigs, + plugins=plugins, name=name, + config_sigs=config_sigs, ) - - # Store driver for external access - self.driver = driver - - async def prepare(self, value: EigerTriggerInfo) -> None: - """Prepare detector with Eiger-specific trigger info.""" - await super().prepare(value) + \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..8eb4f743 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,75 @@ +import asyncio +import pprint + +import pytest +from bluesky.run_engine import RunEngine, TransitionError + + +_ALLOWED_PYTEST_TASKS = {"async_finalizer", "async_setup", "async_teardown"} + +def _error_and_kill_pending_tasks( + loop: asyncio.AbstractEventLoop, test_name: str, test_passed: bool +) -> set[asyncio.Task]: + """Cancels pending tasks in the event loop for a test. Raises an exception if + the test hasn't already. + + Args: + loop: The event loop to check for pending tasks. + test_name: The name of the test. + test_passed: Indicates whether the test passed. + + Returns: + set[asyncio.Task]: The set of unfinished tasks that were cancelled. + + Raises: + RuntimeError: If there are unfinished tasks and the test didn't fail. + """ + unfinished_tasks = { + task + for task in asyncio.all_tasks(loop) + if (coro := task.get_coro()) is not None + and hasattr(coro, "__name__") + and coro.__name__ not in _ALLOWED_PYTEST_TASKS + and not task.done() + } + for task in unfinished_tasks: + task.cancel() + + # We only raise an exception here if the test didn't fail anyway. + # If it did then it makes sense that there's some tasks we need to cancel, + # but an exception will already have been raised. + if unfinished_tasks and test_passed: + raise RuntimeError( + f"Not all tasks closed during test {test_name}:\n" + f"{pprint.pformat(unfinished_tasks, width=88)}" + ) + + return unfinished_tasks + + +@pytest.fixture(scope="function") +def RE(request: pytest.FixtureRequest): + loop = asyncio.new_event_loop() + loop.set_debug(True) + RE = RunEngine({}, call_returns_result=True, loop=loop) + fail_count = request.session.testsfailed + + def clean_event_loop(): + if RE.state not in ("idle", "panicked"): + try: + RE.halt() + except TransitionError: + pass + + loop.call_soon_threadsafe(loop.stop) + RE._th.join() + + try: + _error_and_kill_pending_tasks( + loop, request.node.name, request.session.testsfailed == fail_count + ) + finally: + loop.close() + + request.addfinalizer(clean_event_loop) + return RE diff --git a/tests/test_eiger_async.py b/tests/test_eiger_async.py new file mode 100644 index 00000000..a40b1dab --- /dev/null +++ b/tests/test_eiger_async.py @@ -0,0 +1,276 @@ +""" +Tests for the EigerWriter class using ophyd-async mocking utilities. +""" +import asyncio +from datetime import datetime +from pathlib import Path +from unittest.mock import patch + +from bluesky.run_engine import RunEngine +import pytest +from ophyd_async.core import ( + init_devices, + PathInfo, + StaticPathProvider, + StaticFilenameProvider, +) +from ophyd_async.testing import ( + set_mock_value, + assert_reading, + assert_value, +) + +from cditools.eiger_async import ( + EigerDriverIO, + EigerWriter, + EigerDataSource, + EigerHDF5Format, +) + + +@pytest.fixture +def mock_eiger_driver(RE: RunEngine) -> EigerDriverIO: + """Create a mock EigerDriverIO for testing.""" + with init_devices(mock=True): + driver = EigerDriverIO("MOCK:EIGER:CAM:") + + # Set up some default mock values + set_mock_value(driver.sequence_id, 0.0) + set_mock_value(driver.fw_nimgs_per_file, 1000.0) + set_mock_value(driver.array_size_x, 2048) + set_mock_value(driver.array_size_y, 2048) + set_mock_value(driver.data_type, "UInt16") + + return driver + + +@pytest.fixture +def mock_path_provider(): + """Create a mock path provider for testing.""" + return StaticPathProvider( + StaticFilenameProvider("test_eiger"), + directory_path="/tmp/test_data" + ) + + +@pytest.fixture +def eiger_writer(mock_eiger_driver, mock_path_provider) -> EigerWriter: + """Create an EigerWriter instance for testing.""" + return EigerWriter(mock_eiger_driver, mock_path_provider) + + +@pytest.mark.asyncio +async def test_eiger_writer_initialization(eiger_writer, mock_eiger_driver, mock_path_provider): + """Test that EigerWriter initializes correctly.""" + assert eiger_writer._driver is mock_eiger_driver + assert eiger_writer._path_provider is mock_path_provider + assert eiger_writer._sequence_id_offset == 1 + + +@pytest.mark.asyncio +async def test_eiger_writer_open(eiger_writer, mock_eiger_driver): + """Test the open method configures the detector correctly.""" + # Mock the file path creation + with patch("pathlib.Path.mkdir"), patch("pathlib.Path.exists", return_value=False): + describe_doc = await eiger_writer.open(multiplier=1) + + # Check that the describe document has the expected structure + assert "primary" in describe_doc + primary_doc = describe_doc["primary"] + + assert "source" in primary_doc + assert primary_doc["source"].startswith("EIGER:") + assert primary_doc["shape"] == [2048, 2048] + assert primary_doc["dtype"] == "uint16" + assert "FILESTORE:" in primary_doc["external"] + + # Verify the detector was configured correctly + assert await mock_eiger_driver.fw_enable.get_value() is True + assert await mock_eiger_driver.save_files.get_value() is True + assert await mock_eiger_driver.fw_hdf5_format.get_value() == EigerHDF5Format.V2024_2 + assert await mock_eiger_driver.fw_compression.get_value() is True + assert await mock_eiger_driver.data_source.get_value() == EigerDataSource.FILE_WRITER + + +@pytest.mark.asyncio +async def test_eiger_writer_get_indices_written(eiger_writer, mock_eiger_driver): + """Test getting the number of indices written.""" + # Initialize the writer first + with patch("pathlib.Path.mkdir"), patch("pathlib.Path.exists", return_value=False): + await eiger_writer.open() + + # Initial state should be 0 + assert await eiger_writer.get_indices_written() == 0 + + # Simulate some images being written + set_mock_value(mock_eiger_driver.sequence_id, 3.0) + assert await eiger_writer.get_indices_written() == 3 + + +@pytest.mark.asyncio +async def test_eiger_writer_observe_indices_written(eiger_writer, mock_eiger_driver): + """Test observing indices as they are written.""" + # Initialize the writer first + with patch("pathlib.Path.mkdir"), patch("pathlib.Path.exists", return_value=False): + await eiger_writer.open() + + # Create an async generator to track yielded indices + observed_indices = [] + + async def simulate_writing(): + """Simulate the detector writing images by incrementing sequence_id.""" + await asyncio.sleep(0.05) # Small delay + set_mock_value(mock_eiger_driver.sequence_id, 1.0) + await asyncio.sleep(0.05) + set_mock_value(mock_eiger_driver.sequence_id, 2.0) + await asyncio.sleep(0.05) + set_mock_value(mock_eiger_driver.sequence_id, 3.0) + + # Start the simulation task + sim_task = asyncio.create_task(simulate_writing()) + + # Observe the indices being written + observe_task = asyncio.create_task( + _collect_observed_indices(eiger_writer, observed_indices, max_indices=3) + ) + + # Wait for both tasks to complete + await asyncio.gather(sim_task, observe_task) + + # Should have observed indices 0, 1, 2 + assert observed_indices == [0, 1, 2] + + +async def _collect_observed_indices(writer, indices_list, max_indices): + """Helper function to collect observed indices.""" + count = 0 + async for index in writer.observe_indices_written(timeout=1.0): + indices_list.append(index) + count += 1 + if count >= max_indices: + break + + +@pytest.mark.asyncio +async def test_eiger_writer_collect_stream_docs(eiger_writer, mock_eiger_driver): + """Test collecting stream documents.""" + # Initialize the writer first + with patch("pathlib.Path.mkdir"), patch("pathlib.Path.exists", return_value=False): + await eiger_writer.open() + + # Collect stream docs for 2 indices + docs = [] + async for doc_type, doc in eiger_writer.collect_stream_docs(indices_written=2): + docs.append((doc_type, doc)) + + # Should have 1 stream_resource + 2 stream_datum docs + assert len(docs) == 3 + + # Check stream_resource document + assert docs[0][0] == "stream_resource" + stream_resource = docs[0][1] + assert stream_resource["spec"] == "AD_EIGER" + assert stream_resource["root"] == "/tmp/test_data" + assert "resource_path" in stream_resource + assert stream_resource["resource_kwargs"]["images_per_file"] == 1000 + + # Check stream_datum documents + for i in range(1, 3): + assert docs[i][0] == "stream_datum" + stream_datum = docs[i][1] + assert stream_datum["descriptor"] == "primary" + assert stream_datum["stream_resource"] == stream_resource["uid"] + assert stream_datum["seq_num"] == i - 1 + assert "datum_kwargs" in stream_datum + assert "seq_id" in stream_datum["datum_kwargs"] + + +@pytest.mark.asyncio +async def test_eiger_writer_close(eiger_writer, mock_eiger_driver): + """Test closing the writer.""" + # Initialize the writer first + with patch("pathlib.Path.mkdir"), patch("pathlib.Path.exists", return_value=False): + await eiger_writer.open() + + # Mock file existence check + with patch("pathlib.Path.exists", return_value=True): + await eiger_writer.close() + + # Verify the detector was disabled + assert await mock_eiger_driver.fw_enable.get_value() is False + assert await mock_eiger_driver.save_files.get_value() is False + + +@pytest.mark.asyncio +async def test_eiger_writer_data_type_mapping(eiger_writer, mock_eiger_driver): + """Test that different data types are mapped correctly.""" + data_type_tests = [ + ("UInt8", "uint8"), + ("UInt16", "uint16"), + ("UInt32", "uint32"), + ("Int8", "int8"), + ("Int16", "int16"), + ("Int32", "int32"), + ("Float32", "float32"), + ("Float64", "float64"), + ("Unknown", "uint16"), # Should default to uint16 + ] + + for eiger_type, expected_numpy_type in data_type_tests: + set_mock_value(mock_eiger_driver.data_type, eiger_type) + + with patch("pathlib.Path.mkdir"), patch("pathlib.Path.exists", return_value=False): + describe_doc = await eiger_writer.open() + + assert describe_doc["primary"]["dtype"] == expected_numpy_type + + +@pytest.mark.asyncio +async def test_eiger_writer_file_validation_warning(eiger_writer, mock_eiger_driver, caplog): + """Test that missing files generate warnings.""" + # Initialize the writer first + with patch("pathlib.Path.mkdir"), patch("pathlib.Path.exists", return_value=False): + await eiger_writer.open() + + # Simulate some images being written + set_mock_value(mock_eiger_driver.sequence_id, 2.0) + + # Mock file existence check to return False (files don't exist) + with patch("pathlib.Path.exists", return_value=False): + await eiger_writer.close() + + # Check that a warning was logged about missing files + assert "Master files were not written" in caplog.text + + +@pytest.mark.asyncio +async def test_eiger_writer_resource_uid_generation(eiger_writer): + """Test that resource UIDs are generated consistently.""" + with patch("pathlib.Path.mkdir"), patch("pathlib.Path.exists", return_value=False): + describe_doc1 = await eiger_writer.open() + + # Resource UID should be set and be 8 characters long + assert eiger_writer._resource_uid is not None + assert len(eiger_writer._resource_uid) == 8 + + # Source should include the resource UID + assert eiger_writer._resource_uid in describe_doc1["primary"]["source"] + + +@pytest.mark.asyncio +async def test_eiger_writer_path_creation(eiger_writer): + """Test that write paths are created correctly.""" + with patch("pathlib.Path.mkdir") as mock_mkdir, \ + patch("pathlib.Path.exists", return_value=False): + + # Mock datetime to have consistent path + with patch("cditools.eiger_async.datetime") as mock_datetime: + mock_datetime.now.return_value = datetime(2023, 12, 25, 10, 30, 0) + mock_datetime.strftime = datetime.strftime + + await eiger_writer.open() + + # Check that the path was created + mock_mkdir.assert_called_once() + expected_path = Path("/tmp/test_data/2023/12/25") + assert eiger_writer._write_path == expected_path \ No newline at end of file From 9d28381be3c7cd4d34611a227dfcb54abdc80cda Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Mon, 30 Jun 2025 16:43:45 -0400 Subject: [PATCH 04/46] Progress on file writing --- src/cditools/eiger_async.py | 279 +++++++++++++++++++++++------------- 1 file changed, 178 insertions(+), 101 deletions(-) diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index 4ca097bb..33e6353b 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -1,17 +1,19 @@ """ Ophyd Async implementation for Eiger detector. """ +from dataclasses import dataclass import asyncio -import uuid import time from typing import Sequence, Annotated as A, Any from pathlib import Path from logging import getLogger -from datetime import datetime +import numpy as np +from event_model import DataKey from ophyd_async.core import ( PathProvider, TriggerInfo, SignalRW, SignalR, DetectorTrigger, - StandardDetector, Device, DatasetDescriber, DetectorController, DetectorWriter + StandardDetector, Device, DatasetDescriber, DetectorController, DetectorWriter, + HDFDatasetDescription, HDFDocumentComposer, ) from ophyd_async.epics.signal import PvSuffix from ophyd_async.epics.adcore import ADWriter, NDPluginBaseIO, ADBaseIO, AreaDetector, ADBaseController, ADBaseDatasetDescriber, ADImageMode, ADHDFWriter @@ -231,106 +233,183 @@ class EigerDriverIO(ADBaseIO): energy_eps: A[SignalRW[float], PvSuffix.rbv("EnergyEps")] +@dataclass +class EigerDatasetDescription: + """A subset of the HDFDatasetDescription dataclass for Eiger. + + This is used to describe the different datasets in the master file. + + Attributes: + dataset (str): The dataset path in the HDF5 file. + shape (tuple[int, ...]): The shape of the dataset (excluding the first dimension, which is the number of exposures). + dtype_numpy (str): The numpy dtype of the dataset. + """ + dataset: str + shape: tuple[int, ...] + dtype_numpy: str + + class EigerWriter(DetectorWriter): """Eiger-specific file writer using the built-in FileWriter interface. Adapted from the original eiger.py EigerFileHandler implementation to work with the ophyd-async DetectorWriter interface. + + TODO: How this should work is that the it should only use a single sequence ID for the whole scan. + TODO: Use the num_triggers as the total number of events. + Indices written should be `(num_images // num_triggers) % num_images_counter`. """ - - def __init__(self, driver: EigerDriverIO, path_provider: PathProvider): - self._path_provider = path_provider + + def __init__(self, driver: EigerDriverIO, path_provider: PathProvider, dataset_describer: DatasetDescriber): self._driver = driver + self._path_provider = path_provider + self._dataset_describer = dataset_describer self._sequence_id_offset = 1 - self._resource_uid = None - self._write_path = None - self._file_prefix = None - self._images_per_file = None - self._resource_kwargs = None - self._stream_resource_doc = None - self._initial_sequence_id = None - - async def open(self, multiplier: int = 1) -> dict[str, Any]: + self._initial_sequence_id = 1 + + async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, Any]: """Setup file writing for acquisition.""" # Get file path info from path provider self._file_info = self._path_provider() - - # Generate resource UID similar to original implementation - self._resource_uid = str(uuid.uuid4())[:8] # Similar to new_short_uid() - - # Create write path using current datetime - current_time = datetime.now() - self._write_path = Path(self._file_info.directory_path) / current_time.strftime("%Y/%m/%d") - - # Set up file path on the detector - await self._driver.file_path.set(str(self._write_path)) - + # Set the name pattern with $id replacement similar to original - name_pattern = f"{self._resource_uid}_$id" - await self._driver.fw_name_pattern.set(name_pattern) + name_pattern = f"{self._file_info.filename}_$id" # Configure the Eiger FileWriter await asyncio.gather( + self._driver.file_path.set(self._file_info.directory_path.as_posix()), + self._driver.fw_name_pattern.set(name_pattern), self._driver.fw_enable.set(True), self._driver.save_files.set(True), - self._driver.fw_hdf5_format.set(EigerHDF5Format.V2024_2), - self._driver.fw_compression.set(True), + self._driver.fw_hdf5_format.set(EigerHDF5Format.LEGACY), self._driver.data_source.set(EigerDataSource.FILE_WRITER), ) - # Get images per file for resource document - self._images_per_file = await self._driver.fw_nimgs_per_file.get_value() - - # Set the filename prefix for the resource document - self._file_prefix = self._write_path / self._resource_uid - - # Prepare resource kwargs similar to original implementation - self._resource_kwargs = {"images_per_file": int(self._images_per_file)} - - # Create the stream resource document - self._stream_resource_doc = { - "spec": "AD_EIGER", - "root": str(self._file_info.directory_path), - "resource_path": str(self._file_prefix.relative_to(self._file_info.directory_path)), - "resource_kwargs": self._resource_kwargs, - "path_semantics": "posix", - "uid": str(uuid.uuid4()), - } - - # Validate and create write path - if not self._write_path.exists(): - self._write_path.mkdir(parents=True, exist_ok=True) - - # Get initial sequence ID to track changes + # Get important values from the detector + num_images, num_images_per_file, num_triggers = await asyncio.gather( + self._driver.num_images.get_value(), + self._driver.fw_nimgs_per_file.get_value(), + self._driver.num_triggers.get_value(), + ) + + # Get the initial sequence ID to use to determine the indices written self._initial_sequence_id = await self._driver.sequence_id.get_value() - - # Get dataset info for the describe output - array_size_x = await self._driver.array_size_x.get_value() - array_size_y = await self._driver.array_size_y.get_value() - data_type = await self._driver.data_type.get_value() - - # Convert data type to numpy dtype - dtype_map = { - "UInt8": "uint8", - "UInt16": "uint16", - "UInt32": "uint32", - "Int8": "int8", - "Int16": "int16", - "Int32": "int32", - "Float32": "float32", - "Float64": "float64", - } - dtype = dtype_map.get(data_type, "uint16") # Default to uint16 - - return { - "primary": { - "source": f"EIGER:{self._resource_uid}", - "shape": [int(array_size_y), int(array_size_x)], - "dtype": dtype, - "external": f"FILESTORE:{self._stream_resource_doc['uid']}", - } + + # Exposures per event is a combination of multiple signals, so we can't simply + # set it on the detector, unlike other detector implementations + self._exposures_per_event = num_images * num_triggers + if self._exposures_per_event != exposures_per_event: + msg = ("Mismatch between the calculated exposures per event and the expected exposures per event. " + f"Got {self._exposures_per_event} but expected {exposures_per_event}.") + raise ValueError(msg) + + detector_shape, np_dtype = await asyncio.gather( + self._dataset_describer.shape(), + self._dataset_describer.np_datatype(), + ) + + # Add the master file datasets + master_datasets = [ + HDFDatasetDescription( + data_key=f"{name}_y_pixel_size", + dataset="entry/instrument/detector/y_pixel_size", + shape=(self._exposures_per_event,), + dtype_numpy=np.dtype(np.float32).str, + chunk_shape=(1,), + ), + HDFDatasetDescription( + data_key=f"{name}_x_pixel_size", + dataset="entry/instrument/detector/x_pixel_size", + shape=(self._exposures_per_event,), + dtype_numpy=np.dtype(np.float32).str, + chunk_shape=(1,), + ), + HDFDatasetDescription( + data_key=f"{name}_detector_distance", + dataset="entry/instrument/detector/distance", + shape=(self._exposures_per_event,), + dtype_numpy=np.dtype(np.float32).str, + chunk_shape=(1,), + ), + HDFDatasetDescription( + data_key=f"{name}_incident_wavelength", + dataset="entry/instrument/detector/incident_wavelength", + shape=(self._exposures_per_event,), + dtype_numpy=np.dtype(np.float32).str, + chunk_shape=(1,), + ), + HDFDatasetDescription( + data_key=f"{name}_frame_time", + dataset="entry/instrument/detector/frame_time", + shape=(self._exposures_per_event,), + dtype_numpy=np.dtype(np.float32).str, + chunk_shape=(1,), + ), + HDFDatasetDescription( + data_key=f"{name}_beam_center_x", + dataset="entry/instrument/detector/beam_center_x", + shape=(self._exposures_per_event,), + dtype_numpy=np.dtype(np.float32).str, + chunk_shape=(1,), + ), + HDFDatasetDescription( + data_key=f"{name}_beam_center_y", + dataset="entry/instrument/detector/beam_center_y", + shape=(self._exposures_per_event,), + dtype_numpy=np.dtype(np.float32).str, + chunk_shape=(1,), + ), + HDFDatasetDescription( + data_key=f"{name}_count_time", + dataset="entry/instrument/detector/count_time", + shape=(self._exposures_per_event,), + dtype_numpy=np.dtype(np.float32).str, + chunk_shape=(1,), + ), + HDFDatasetDescription( + data_key=f"{name}_pixel_mask", + dataset="entry/instrument/detector/detectorSpecific/pixel_mask", + # TODO: Maybe only 1 mask? + shape=(self._exposures_per_event, *detector_shape), + dtype_numpy=np.dtype(np.uint8).str, + chunk_shape=(1, *detector_shape), + ), + ] + + # Add the array datasets (linked from the master file) + # Linked keys are of the form + # - "/entry/data_000001" + # - "/entry/data_000002" + # - ... + # Example: if exposures_per_event (num_images) is 60, num_triggers is 2, and num_images_per_file is 100, + # then the data_000001 file will have 100 images and the data_000002 filewill have 20 images. + # Put simply, the last file could have less than num_images_per_file images. + frame_datasets = [ + HDFDatasetDescription( + data_key=f"{name}_{i}", + dataset=f"/entry/data_{i:06d}", + shape=(min(num_images_per_file, self._exposures_per_event - (i - 1) * num_images_per_file), *detector_shape), + dtype_numpy=np_dtype, + chunk_shape=(1, *detector_shape), + ) + for i in range(1, num_triggers + 1) + ] + + self._datasets = master_datasets + frame_datasets + + describe = { + ds.data_key: DataKey( + source=await self._driver.file_path.get_value(), + shape=list(ds.shape), + dtype="array" if self._exposures_per_event > 1 or len(ds.shape) > 1 else "number", + dtype_numpy=ds.dtype_numpy, + external="STREAM:", + ) + for ds in self._datasets } + return describe + async def observe_indices_written(self, timeout: float = 10.0): """Monitor the number of files written by the Eiger FileWriter.""" last_sequence_id = self._initial_sequence_id @@ -357,30 +436,27 @@ async def get_indices_written(self) -> int: current_sequence_id = await self._driver.sequence_id.get_value() return int(current_sequence_id - self._initial_sequence_id) - async def collect_stream_docs(self, indices_written: int): + async def collect_stream_docs(self, name: str, indices_written: int) -> AsyncIterator[StreamAsset]: """Generate stream documents for the written HDF5 files. Follows the pattern from the original EigerFileHandler.generate_datum method. """ - if self._stream_resource_doc is None: - return - - # Yield the stream resource document first - yield "stream_resource", self._stream_resource_doc - - # Generate stream datum documents for each index written - for i in range(indices_written): - # Calculate the actual sequence number like in original implementation - sequence_number = self._sequence_id_offset + self._initial_sequence_id + i - - stream_datum = { - "descriptor": "primary", - "stream_resource": self._stream_resource_doc["uid"], - "seq_num": i, - "timestamps": [time.time()], - "datum_kwargs": {"seq_id": int(sequence_number)}, - } - yield "stream_datum", stream_datum + # TODO: Is this needed? + await self._driver.fw_state.wait_for_value(EigerFileWriterState.IDLE) + if indices_written: + if not self._composer: + # TODO: Use master file path... + path = Path(await self._driver.file_path.get_value()) + self._composer = HDFDocumentComposer( + path, + self._datasets, + ) + + for doc in self._composer.stream_resources(): + yield "stream_resource", doc + + for doc in self._composer.stream_data(indices_written): + yield "stream_datum", doc async def close(self) -> None: """Clean up file writing after acquisition and validate files exist.""" @@ -438,7 +514,8 @@ async def prepare(self, trigger_info: TriggerInfo) -> None: image_mode = ADImageMode.MULTIPLE await asyncio.gather( - self._driver.num_images.set(trigger_info.total_number_of_exposures), + self._driver.num_triggers.set(trigger_info.number_of_events), + self._driver.num_images.set(trigger_info.exposures_per_event), self._driver.image_mode.set(image_mode), ) From 929ea010b34f1d9e84c55a850faadb69082c4994 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Tue, 1 Jul 2025 11:04:49 -0400 Subject: [PATCH 05/46] Finished first version of EigerWriter (untested) --- src/cditools/eiger_async.py | 162 +++++++++++++++--------------------- tests/test_eiger_async.py | 94 +++++++++++++++------ 2 files changed, 136 insertions(+), 120 deletions(-) diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index 33e6353b..94369355 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -1,22 +1,22 @@ """ Ophyd Async implementation for Eiger detector. """ -from dataclasses import dataclass import asyncio -import time from typing import Sequence, Annotated as A, Any from pathlib import Path from logging import getLogger +from collections.abc import AsyncGenerator, AsyncIterator +from bluesky.protocols import StreamAsset import numpy as np from event_model import DataKey from ophyd_async.core import ( PathProvider, TriggerInfo, SignalRW, SignalR, DetectorTrigger, - StandardDetector, Device, DatasetDescriber, DetectorController, DetectorWriter, - HDFDatasetDescription, HDFDocumentComposer, + DatasetDescriber, DetectorWriter, + HDFDatasetDescription, HDFDocumentComposer, observe_value, PathInfo ) from ophyd_async.epics.signal import PvSuffix -from ophyd_async.epics.adcore import ADWriter, NDPluginBaseIO, ADBaseIO, AreaDetector, ADBaseController, ADBaseDatasetDescriber, ADImageMode, ADHDFWriter +from ophyd_async.epics.adcore import NDPluginBaseIO, ADBaseIO, AreaDetector, ADBaseController, ADImageMode from ophyd_async.core import StrictEnum logger = getLogger(__name__) @@ -108,6 +108,7 @@ class EigerDriverIO(ADBaseIO): # Standard Driver Parameters trigger_mode: A[SignalRW[EigerTriggerMode], PvSuffix.rbv("TriggerMode")] num_images: A[SignalRW[int], PvSuffix.rbv("NumImages")] + num_images_counter: A[SignalR[int], PvSuffix("NumImagesCounter_RBV")] num_exposures: A[SignalRW[int], PvSuffix.rbv("NumExposures")] acquire_time: A[SignalRW[float], PvSuffix.rbv("AcquireTime")] acquire_period: A[SignalRW[float], PvSuffix.rbv("AcquirePeriod")] @@ -233,44 +234,23 @@ class EigerDriverIO(ADBaseIO): energy_eps: A[SignalRW[float], PvSuffix.rbv("EnergyEps")] -@dataclass -class EigerDatasetDescription: - """A subset of the HDFDatasetDescription dataclass for Eiger. - - This is used to describe the different datasets in the master file. - - Attributes: - dataset (str): The dataset path in the HDF5 file. - shape (tuple[int, ...]): The shape of the dataset (excluding the first dimension, which is the number of exposures). - dtype_numpy (str): The numpy dtype of the dataset. - """ - dataset: str - shape: tuple[int, ...] - dtype_numpy: str - - class EigerWriter(DetectorWriter): - """Eiger-specific file writer using the built-in FileWriter interface. - - Adapted from the original eiger.py EigerFileHandler implementation - to work with the ophyd-async DetectorWriter interface. - - TODO: How this should work is that the it should only use a single sequence ID for the whole scan. - TODO: Use the num_triggers as the total number of events. - Indices written should be `(num_images // num_triggers) % num_images_counter`. - """ + """Eiger-specific file writer using the built-in FileWriter interface.""" def __init__(self, driver: EigerDriverIO, path_provider: PathProvider, dataset_describer: DatasetDescriber): self._driver = driver self._path_provider = path_provider self._dataset_describer = dataset_describer - self._sequence_id_offset = 1 - self._initial_sequence_id = 1 - async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, Any]: + self._file_info: PathInfo | None = None + self._current_sequence_id: int | None = None + self._composer: HDFDocumentComposer | None = None + + async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataKey]: """Setup file writing for acquisition.""" # Get file path info from path provider self._file_info = self._path_provider() + self._current_sequence_id = await self._driver.sequence_id.get_value() # Set the name pattern with $id replacement similar to original name_pattern = f"{self._file_info.filename}_$id" @@ -292,15 +272,12 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, Any]: self._driver.num_triggers.get_value(), ) - # Get the initial sequence ID to use to determine the indices written - self._initial_sequence_id = await self._driver.sequence_id.get_value() - # Exposures per event is a combination of multiple signals, so we can't simply # set it on the detector, unlike other detector implementations - self._exposures_per_event = num_images * num_triggers - if self._exposures_per_event != exposures_per_event: - msg = ("Mismatch between the calculated exposures per event and the expected exposures per event. " - f"Got {self._exposures_per_event} but expected {exposures_per_event}.") + if num_images != exposures_per_event: + msg = ("Mismatch between the number of images set in the detector " + "and the expected number of exposures per event. " + f"Got {num_images} but expected {exposures_per_event}.") raise ValueError(msg) detector_shape, np_dtype = await asyncio.gather( @@ -313,56 +290,56 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, Any]: HDFDatasetDescription( data_key=f"{name}_y_pixel_size", dataset="entry/instrument/detector/y_pixel_size", - shape=(self._exposures_per_event,), + shape=(exposures_per_event,), dtype_numpy=np.dtype(np.float32).str, chunk_shape=(1,), ), HDFDatasetDescription( data_key=f"{name}_x_pixel_size", dataset="entry/instrument/detector/x_pixel_size", - shape=(self._exposures_per_event,), + shape=(exposures_per_event,), dtype_numpy=np.dtype(np.float32).str, chunk_shape=(1,), ), HDFDatasetDescription( data_key=f"{name}_detector_distance", dataset="entry/instrument/detector/distance", - shape=(self._exposures_per_event,), + shape=(exposures_per_event,), dtype_numpy=np.dtype(np.float32).str, chunk_shape=(1,), ), HDFDatasetDescription( data_key=f"{name}_incident_wavelength", dataset="entry/instrument/detector/incident_wavelength", - shape=(self._exposures_per_event,), + shape=(exposures_per_event,), dtype_numpy=np.dtype(np.float32).str, chunk_shape=(1,), ), HDFDatasetDescription( data_key=f"{name}_frame_time", dataset="entry/instrument/detector/frame_time", - shape=(self._exposures_per_event,), + shape=(exposures_per_event,), dtype_numpy=np.dtype(np.float32).str, chunk_shape=(1,), ), HDFDatasetDescription( data_key=f"{name}_beam_center_x", dataset="entry/instrument/detector/beam_center_x", - shape=(self._exposures_per_event,), + shape=(exposures_per_event,), dtype_numpy=np.dtype(np.float32).str, chunk_shape=(1,), ), HDFDatasetDescription( data_key=f"{name}_beam_center_y", dataset="entry/instrument/detector/beam_center_y", - shape=(self._exposures_per_event,), + shape=(exposures_per_event,), dtype_numpy=np.dtype(np.float32).str, chunk_shape=(1,), ), HDFDatasetDescription( data_key=f"{name}_count_time", dataset="entry/instrument/detector/count_time", - shape=(self._exposures_per_event,), + shape=(exposures_per_event,), dtype_numpy=np.dtype(np.float32).str, chunk_shape=(1,), ), @@ -370,7 +347,7 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, Any]: data_key=f"{name}_pixel_mask", dataset="entry/instrument/detector/detectorSpecific/pixel_mask", # TODO: Maybe only 1 mask? - shape=(self._exposures_per_event, *detector_shape), + shape=(exposures_per_event, *detector_shape), dtype_numpy=np.dtype(np.uint8).str, chunk_shape=(1, *detector_shape), ), @@ -388,7 +365,7 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, Any]: HDFDatasetDescription( data_key=f"{name}_{i}", dataset=f"/entry/data_{i:06d}", - shape=(min(num_images_per_file, self._exposures_per_event - (i - 1) * num_images_per_file), *detector_shape), + shape=(min(num_images_per_file, exposures_per_event - (i - 1) * num_images_per_file), *detector_shape), dtype_numpy=np_dtype, chunk_shape=(1, *detector_shape), ) @@ -399,9 +376,9 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, Any]: describe = { ds.data_key: DataKey( - source=await self._driver.file_path.get_value(), + source=self._master_file_path.as_posix(), shape=list(ds.shape), - dtype="array" if self._exposures_per_event > 1 or len(ds.shape) > 1 else "number", + dtype="array" if exposures_per_event > 1 or len(ds.shape) > 1 else "number", dtype_numpy=ds.dtype_numpy, external="STREAM:", ) @@ -410,45 +387,49 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, Any]: return describe - async def observe_indices_written(self, timeout: float = 10.0): + @property + def _master_file_path(self) -> Path: + if self._current_sequence_id is None or self._file_info is None: + raise ValueError("Must call EigerWriter.open() before accessing master file path") + return self._file_info.directory_path / f"{self._file_info.filename}_{self._current_sequence_id}_master.h5" + + @staticmethod + def compute_index(num_images_counter: int, num_triggers: int, num_images: int) -> int: + return (num_images_counter // num_triggers) % num_images + + async def observe_indices_written(self, timeout: float = 10.0) -> AsyncGenerator[int, None]: """Monitor the number of files written by the Eiger FileWriter.""" - last_sequence_id = self._initial_sequence_id - - while True: - try: - # Check sequence ID to see how many acquisitions have been written - current_sequence_id = await self._driver.sequence_id.get_value() - - if current_sequence_id > last_sequence_id: - # Yield indices for all new sequences since last check - for seq_id in range(int(last_sequence_id + 1), int(current_sequence_id + 1)): - # Convert sequence ID to 0-based index - index = seq_id - self._initial_sequence_id - 1 - yield index - last_sequence_id = current_sequence_id - - await asyncio.sleep(0.1) - except asyncio.TimeoutError: - break + async for num_images_counter in observe_value(self._driver.num_images_counter, timeout): + num_triggers, num_images = await asyncio.gather( + self._driver.num_triggers.get_value(), + self._driver.num_images.get_value(), + ) + yield self.compute_index(num_images_counter, num_triggers, num_images) async def get_indices_written(self) -> int: - """Get the current number of indices written.""" - current_sequence_id = await self._driver.sequence_id.get_value() - return int(current_sequence_id - self._initial_sequence_id) + """Get the current number of indices written. + + Since Eiger defines the number of triggers, we want each trigger + to correspond to a single index. This is in contrast to the ophyd-sync + implementation, where the `num_triggers` was ignored. + """ + + num_images_counter, num_triggers, num_images = await asyncio.gather( + self._driver.num_images_counter.get_value(), + self._driver.num_triggers.get_value(), + self._driver.num_images.get_value(), + ) + return self.compute_index(num_images_counter, num_triggers, num_images) async def collect_stream_docs(self, name: str, indices_written: int) -> AsyncIterator[StreamAsset]: """Generate stream documents for the written HDF5 files. Follows the pattern from the original EigerFileHandler.generate_datum method. """ - # TODO: Is this needed? - await self._driver.fw_state.wait_for_value(EigerFileWriterState.IDLE) if indices_written: if not self._composer: - # TODO: Use master file path... - path = Path(await self._driver.file_path.get_value()) self._composer = HDFDocumentComposer( - path, + self._master_file_path, self._datasets, ) @@ -465,22 +446,13 @@ async def close(self) -> None: self._driver.fw_enable.set(False), self._driver.save_files.set(False), ) - - # Validate that master files were written (similar to original unstage method) - if self._file_prefix is not None and self._initial_sequence_id is not None: - final_sequence_id = await self._driver.sequence_id.get_value() - indices_written = int(final_sequence_id - self._initial_sequence_id) - - # Check that master files exist for each sequence - missing_files = [] - for i in range(indices_written): - sequence_number = self._sequence_id_offset + self._initial_sequence_id + i - master_file = Path(f"{self._file_prefix}_{int(sequence_number)}_master.h5") - if not master_file.exists(): - missing_files.append(master_file) - - if missing_files: - logger.warning(f"Master files were not written: {missing_files}") + + if not self._master_file_path.exists(): + logger.warning(f"Master file was not written: {self._master_file_path}") + + self._composer = None + self._file_info = None + self._current_sequence_id = None class EigerController(ADBaseController[EigerDriverIO]): diff --git a/tests/test_eiger_async.py b/tests/test_eiger_async.py index a40b1dab..ccace53a 100644 --- a/tests/test_eiger_async.py +++ b/tests/test_eiger_async.py @@ -9,11 +9,13 @@ from bluesky.run_engine import RunEngine import pytest from ophyd_async.core import ( + PathProvider, init_devices, PathInfo, StaticPathProvider, StaticFilenameProvider, ) +from ophyd_async.epics.adcore import ADBaseDatasetDescriber from ophyd_async.testing import ( set_mock_value, assert_reading, @@ -35,8 +37,6 @@ def mock_eiger_driver(RE: RunEngine) -> EigerDriverIO: driver = EigerDriverIO("MOCK:EIGER:CAM:") # Set up some default mock values - set_mock_value(driver.sequence_id, 0.0) - set_mock_value(driver.fw_nimgs_per_file, 1000.0) set_mock_value(driver.array_size_x, 2048) set_mock_value(driver.array_size_y, 2048) set_mock_value(driver.data_type, "UInt16") @@ -45,7 +45,7 @@ def mock_eiger_driver(RE: RunEngine) -> EigerDriverIO: @pytest.fixture -def mock_path_provider(): +def mock_path_provider() -> PathProvider: """Create a mock path provider for testing.""" return StaticPathProvider( StaticFilenameProvider("test_eiger"), @@ -54,42 +54,86 @@ def mock_path_provider(): @pytest.fixture -def eiger_writer(mock_eiger_driver, mock_path_provider) -> EigerWriter: +def eiger_writer(mock_eiger_driver: EigerDriverIO, mock_path_provider: PathProvider) -> EigerWriter: """Create an EigerWriter instance for testing.""" - return EigerWriter(mock_eiger_driver, mock_path_provider) + dataset_describer = ADBaseDatasetDescriber(mock_eiger_driver) + return EigerWriter(mock_eiger_driver, mock_path_provider, dataset_describer) @pytest.mark.asyncio -async def test_eiger_writer_initialization(eiger_writer, mock_eiger_driver, mock_path_provider): +async def test_eiger_writer_initialization(eiger_writer: EigerWriter, mock_eiger_driver: EigerDriverIO, mock_path_provider: PathProvider): """Test that EigerWriter initializes correctly.""" assert eiger_writer._driver is mock_eiger_driver assert eiger_writer._path_provider is mock_path_provider - assert eiger_writer._sequence_id_offset == 1 + assert eiger_writer._dataset_describer is not None + assert eiger_writer._file_info is None + assert eiger_writer._current_sequence_id is None + assert eiger_writer._composer is None @pytest.mark.asyncio -async def test_eiger_writer_open(eiger_writer, mock_eiger_driver): +async def test_eiger_writer_open(eiger_writer: EigerWriter, mock_eiger_driver: EigerDriverIO) -> None: """Test the open method configures the detector correctly.""" - # Mock the file path creation - with patch("pathlib.Path.mkdir"), patch("pathlib.Path.exists", return_value=False): - describe_doc = await eiger_writer.open(multiplier=1) - - # Check that the describe document has the expected structure - assert "primary" in describe_doc - primary_doc = describe_doc["primary"] - - assert "source" in primary_doc - assert primary_doc["source"].startswith("EIGER:") - assert primary_doc["shape"] == [2048, 2048] - assert primary_doc["dtype"] == "uint16" - assert "FILESTORE:" in primary_doc["external"] - - # Verify the detector was configured correctly + array_size_x, array_size_y, data_type = asyncio.gather( + mock_eiger_driver.array_size_x.get_value(), + mock_eiger_driver.array_size_y.get_value(), + mock_eiger_driver.data_type.get_value(), + ) + + # Case 1: 1 image per file, 1 image, 1 trigger + set_mock_value(mock_eiger_driver.fw_nimgs_per_file, 1) + set_mock_value(mock_eiger_driver.num_images, 1) + set_mock_value(mock_eiger_driver.num_triggers, 1) + + description = await eiger_writer.open(name="test_eiger", exposures_per_event=1) assert await mock_eiger_driver.fw_enable.get_value() is True assert await mock_eiger_driver.save_files.get_value() is True - assert await mock_eiger_driver.fw_hdf5_format.get_value() == EigerHDF5Format.V2024_2 - assert await mock_eiger_driver.fw_compression.get_value() is True + assert await mock_eiger_driver.fw_hdf5_format.get_value() == EigerHDF5Format.LEGACY assert await mock_eiger_driver.data_source.get_value() == EigerDataSource.FILE_WRITER + assert description.keys() == ["test_eiger_y_pixel_size", + "test_eiger_x_pixel_size", + "test_eiger_detector_distance", + "test_eiger_incident_wavelength", + "test_eiger_frame_time", + "test_eiger_beam_center_x", + "test_eiger_beam_center_y", + "test_eiger_count_time", + "test_eiger_pixel_mask", + "test_eiger_1"] + + # Case 2: 4 images per file, 11 images, 2 triggers + # Expect 6 files, the first 5 will have 4 images, the last will have 2 + set_mock_value(mock_eiger_driver.fw_nimgs_per_file, 4) + set_mock_value(mock_eiger_driver.num_images, 11) + set_mock_value(mock_eiger_driver.num_triggers, 2) + description = await eiger_writer.open(name="test_eiger", exposures_per_event=await mock_eiger_driver.num_images.get_value()) + assert description.keys() == ["test_eiger_y_pixel_size", + "test_eiger_x_pixel_size", + "test_eiger_detector_distance", + "test_eiger_incident_wavelength", + "test_eiger_frame_time", + "test_eiger_beam_center_x", + "test_eiger_beam_center_y", + "test_eiger_count_time", + "test_eiger_pixel_mask", + "test_eiger_1", + "test_eiger_2", + "test_eiger_3", + "test_eiger_4", + "test_eiger_5", + "test_eiger_6"] + + for i in range(1, 6): + data_key = description[f"test_eiger_{i}"] + assert data_key.shape == (4, array_size_x, array_size_y) + assert data_key.dtype == data_type + assert data_key.external == "STREAM:" + assert data_key.source == f"/tmp/test_data/test_eiger_1_master.h5" + data_key = description["test_eiger_6"] + assert data_key.shape == (2, array_size_x, array_size_y) + assert data_key.dtype == data_type + assert data_key.external == "STREAM:" + assert data_key.source == f"/tmp/test_data/test_eiger_1_master.h5" @pytest.mark.asyncio From 9a8f1f39030abc9a3c164e1dfb1e493efee4b714 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Tue, 1 Jul 2025 16:53:12 -0400 Subject: [PATCH 06/46] Finished writing unit tests for eiger async --- src/cditools/eiger_async.py | 41 ++--- tests/test_eiger_async.py | 341 ++++++++++++++++++------------------ 2 files changed, 186 insertions(+), 196 deletions(-) diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index 94369355..ee5231a6 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -2,6 +2,7 @@ Ophyd Async implementation for Eiger detector. """ import asyncio +from math import ceil from typing import Sequence, Annotated as A, Any from pathlib import Path from logging import getLogger @@ -160,7 +161,7 @@ class EigerDriverIO(ADBaseIO): ext_gate_mode: A[SignalRW[str], PvSuffix.rbv("ExtGateMode")] trigger: A[SignalRW[float], PvSuffix("Trigger")] trigger_exposure: A[SignalRW[float], PvSuffix.rbv("TriggerExposure")] - num_triggers: A[SignalRW[float], PvSuffix.rbv("NumTriggers")] + num_triggers: A[SignalRW[int], PvSuffix.rbv("NumTriggers")] manual_trigger: A[SignalRW[bool], PvSuffix.rbv("ManualTrigger")] trigger_start_delay: A[SignalRW[float], PvSuffix.rbv("TriggerStartDelay")] @@ -186,7 +187,7 @@ class EigerDriverIO(ADBaseIO): fw_compression: A[SignalRW[bool], PvSuffix.rbv("FWCompression")] fw_nimgs_per_file: A[SignalRW[float], PvSuffix.rbv("FWNImgsPerFile")] fw_name_pattern: A[SignalRW[str], PvSuffix.rbv("FWNamePattern")] - sequence_id: A[SignalR[float], PvSuffix("SequenceId")] + sequence_id: A[SignalR[int], PvSuffix("SequenceId")] save_files: A[SignalRW[bool], PvSuffix.rbv("SaveFiles")] file_path: A[SignalRW[str], PvSuffix.rbv("FilePath")] file_owner: A[SignalRW[str], PvSuffix.rbv("FileOwner")] @@ -245,6 +246,7 @@ def __init__(self, driver: EigerDriverIO, path_provider: PathProvider, dataset_d self._file_info: PathInfo | None = None self._current_sequence_id: int | None = None self._composer: HDFDocumentComposer | None = None + self._exposures_per_event: int = 1 async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataKey]: """Setup file writing for acquisition.""" @@ -353,6 +355,9 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK ), ] + # Cache for use later + self._exposures_per_event = exposures_per_event + # Add the array datasets (linked from the master file) # Linked keys are of the form # - "/entry/data_000001" @@ -361,15 +366,16 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK # Example: if exposures_per_event (num_images) is 60, num_triggers is 2, and num_images_per_file is 100, # then the data_000001 file will have 100 images and the data_000002 filewill have 20 images. # Put simply, the last file could have less than num_images_per_file images. + total_images = num_triggers * exposures_per_event frame_datasets = [ HDFDatasetDescription( data_key=f"{name}_{i}", dataset=f"/entry/data_{i:06d}", - shape=(min(num_images_per_file, exposures_per_event - (i - 1) * num_images_per_file), *detector_shape), + shape=(min(num_images_per_file, total_images - (i - 1) * num_images_per_file), *detector_shape), dtype_numpy=np_dtype, chunk_shape=(1, *detector_shape), ) - for i in range(1, num_triggers + 1) + for i in range(1, ceil(total_images / num_images_per_file) + 1) ] self._datasets = master_datasets + frame_datasets @@ -393,18 +399,16 @@ def _master_file_path(self) -> Path: raise ValueError("Must call EigerWriter.open() before accessing master file path") return self._file_info.directory_path / f"{self._file_info.filename}_{self._current_sequence_id}_master.h5" - @staticmethod - def compute_index(num_images_counter: int, num_triggers: int, num_images: int) -> int: - return (num_images_counter // num_triggers) % num_images + async def compute_index(self, num_images_counter: int) -> int: + if await self._driver.num_images.get_value() != self._exposures_per_event: + raise RuntimeError("Detected change to the number of images during acquisition. This is not allowed.") + return num_images_counter // self._exposures_per_event async def observe_indices_written(self, timeout: float = 10.0) -> AsyncGenerator[int, None]: """Monitor the number of files written by the Eiger FileWriter.""" async for num_images_counter in observe_value(self._driver.num_images_counter, timeout): - num_triggers, num_images = await asyncio.gather( - self._driver.num_triggers.get_value(), - self._driver.num_images.get_value(), - ) - yield self.compute_index(num_images_counter, num_triggers, num_images) + print(f"{num_images_counter=}") + yield await self.compute_index(num_images_counter) async def get_indices_written(self) -> int: """Get the current number of indices written. @@ -413,19 +417,10 @@ async def get_indices_written(self) -> int: to correspond to a single index. This is in contrast to the ophyd-sync implementation, where the `num_triggers` was ignored. """ - - num_images_counter, num_triggers, num_images = await asyncio.gather( - self._driver.num_images_counter.get_value(), - self._driver.num_triggers.get_value(), - self._driver.num_images.get_value(), - ) - return self.compute_index(num_images_counter, num_triggers, num_images) + return await self.compute_index(await self._driver.num_images_counter.get_value()) async def collect_stream_docs(self, name: str, indices_written: int) -> AsyncIterator[StreamAsset]: - """Generate stream documents for the written HDF5 files. - - Follows the pattern from the original EigerFileHandler.generate_datum method. - """ + """Generate stream documents for the written HDF5 files.""" if indices_written: if not self._composer: self._composer = HDFDocumentComposer( diff --git a/tests/test_eiger_async.py b/tests/test_eiger_async.py index ccace53a..4d2bfdb2 100644 --- a/tests/test_eiger_async.py +++ b/tests/test_eiger_async.py @@ -6,6 +6,7 @@ from pathlib import Path from unittest.mock import patch +import numpy as np from bluesky.run_engine import RunEngine import pytest from ophyd_async.core import ( @@ -74,13 +75,14 @@ async def test_eiger_writer_initialization(eiger_writer: EigerWriter, mock_eiger @pytest.mark.asyncio async def test_eiger_writer_open(eiger_writer: EigerWriter, mock_eiger_driver: EigerDriverIO) -> None: """Test the open method configures the detector correctly.""" - array_size_x, array_size_y, data_type = asyncio.gather( + array_size_x, array_size_y, data_type = await asyncio.gather( mock_eiger_driver.array_size_x.get_value(), mock_eiger_driver.array_size_y.get_value(), mock_eiger_driver.data_type.get_value(), ) # Case 1: 1 image per file, 1 image, 1 trigger + set_mock_value(mock_eiger_driver.sequence_id, 1) set_mock_value(mock_eiger_driver.fw_nimgs_per_file, 1) set_mock_value(mock_eiger_driver.num_images, 1) set_mock_value(mock_eiger_driver.num_triggers, 1) @@ -90,7 +92,7 @@ async def test_eiger_writer_open(eiger_writer: EigerWriter, mock_eiger_driver: E assert await mock_eiger_driver.save_files.get_value() is True assert await mock_eiger_driver.fw_hdf5_format.get_value() == EigerHDF5Format.LEGACY assert await mock_eiger_driver.data_source.get_value() == EigerDataSource.FILE_WRITER - assert description.keys() == ["test_eiger_y_pixel_size", + assert description.keys() == {"test_eiger_y_pixel_size", "test_eiger_x_pixel_size", "test_eiger_detector_distance", "test_eiger_incident_wavelength", @@ -99,15 +101,17 @@ async def test_eiger_writer_open(eiger_writer: EigerWriter, mock_eiger_driver: E "test_eiger_beam_center_y", "test_eiger_count_time", "test_eiger_pixel_mask", - "test_eiger_1"] + "test_eiger_1"} + assert description["test_eiger_1"]["source"] == "/tmp/test_data/test_eiger_1_master.h5" # Case 2: 4 images per file, 11 images, 2 triggers # Expect 6 files, the first 5 will have 4 images, the last will have 2 + set_mock_value(mock_eiger_driver.sequence_id, 2) set_mock_value(mock_eiger_driver.fw_nimgs_per_file, 4) set_mock_value(mock_eiger_driver.num_images, 11) set_mock_value(mock_eiger_driver.num_triggers, 2) description = await eiger_writer.open(name="test_eiger", exposures_per_event=await mock_eiger_driver.num_images.get_value()) - assert description.keys() == ["test_eiger_y_pixel_size", + assert description.keys() == {"test_eiger_y_pixel_size", "test_eiger_x_pixel_size", "test_eiger_detector_distance", "test_eiger_incident_wavelength", @@ -121,200 +125,191 @@ async def test_eiger_writer_open(eiger_writer: EigerWriter, mock_eiger_driver: E "test_eiger_3", "test_eiger_4", "test_eiger_5", - "test_eiger_6"] + "test_eiger_6"} for i in range(1, 6): data_key = description[f"test_eiger_{i}"] - assert data_key.shape == (4, array_size_x, array_size_y) - assert data_key.dtype == data_type - assert data_key.external == "STREAM:" - assert data_key.source == f"/tmp/test_data/test_eiger_1_master.h5" + assert tuple(data_key["shape"]) == (4, array_size_x, array_size_y) + assert data_key["dtype"] == "array" + assert data_key["dtype_numpy"] == np.dtype(data_type.lower()).str + assert data_key["external"] == "STREAM:" + assert data_key["source"] == f"/tmp/test_data/test_eiger_2_master.h5" data_key = description["test_eiger_6"] - assert data_key.shape == (2, array_size_x, array_size_y) - assert data_key.dtype == data_type - assert data_key.external == "STREAM:" - assert data_key.source == f"/tmp/test_data/test_eiger_1_master.h5" + assert tuple(data_key["shape"]) == (2, array_size_x, array_size_y) + assert data_key["dtype"] == "array" + assert data_key["dtype_numpy"] == np.dtype(data_type.lower()).str + assert data_key["external"] == "STREAM:" + assert data_key["source"] == f"/tmp/test_data/test_eiger_2_master.h5" @pytest.mark.asyncio async def test_eiger_writer_get_indices_written(eiger_writer, mock_eiger_driver): """Test getting the number of indices written.""" - # Initialize the writer first - with patch("pathlib.Path.mkdir"), patch("pathlib.Path.exists", return_value=False): - await eiger_writer.open() - - # Initial state should be 0 + set_mock_value(mock_eiger_driver.sequence_id, 1) + set_mock_value(mock_eiger_driver.fw_nimgs_per_file, 1) + + # Case 1: 1 image, 1 trigger + set_mock_value(mock_eiger_driver.num_triggers, 1) + set_mock_value(mock_eiger_driver.num_images, 1) + set_mock_value(mock_eiger_driver.num_images_counter, 0) + await eiger_writer.open( + name="test_eiger", + exposures_per_event=await mock_eiger_driver.num_images.get_value()) assert await eiger_writer.get_indices_written() == 0 - - # Simulate some images being written - set_mock_value(mock_eiger_driver.sequence_id, 3.0) + set_mock_value(mock_eiger_driver.num_images_counter, 1) + assert await eiger_writer.get_indices_written() == 1 + + # Case 2: 1 image, 5 triggers + set_mock_value(mock_eiger_driver.num_triggers, 5) + set_mock_value(mock_eiger_driver.num_images, 1) + set_mock_value(mock_eiger_driver.num_images_counter, 0) + await eiger_writer.open( + name="test_eiger", + exposures_per_event=await mock_eiger_driver.num_images.get_value()) + assert await eiger_writer.get_indices_written() == 0 + set_mock_value(mock_eiger_driver.num_images_counter, 1) + assert await eiger_writer.get_indices_written() == 1 + set_mock_value(mock_eiger_driver.num_images_counter, 3) assert await eiger_writer.get_indices_written() == 3 + set_mock_value(mock_eiger_driver.num_images_counter, 5) + assert await eiger_writer.get_indices_written() == 5 + + # Case 3: 5 images, 2 triggers + set_mock_value(mock_eiger_driver.num_triggers, 2) + set_mock_value(mock_eiger_driver.num_images, 5) + set_mock_value(mock_eiger_driver.num_images_counter, 0) + await eiger_writer.open( + name="test_eiger", + exposures_per_event=await mock_eiger_driver.num_images.get_value()) + assert await eiger_writer.get_indices_written() == 0 + set_mock_value(mock_eiger_driver.num_images_counter, 4) + assert await eiger_writer.get_indices_written() == 0 + set_mock_value(mock_eiger_driver.num_images_counter, 5) + assert await eiger_writer.get_indices_written() == 1 + set_mock_value(mock_eiger_driver.num_images_counter, 9) + assert await eiger_writer.get_indices_written() == 1 + set_mock_value(mock_eiger_driver.num_images_counter, 10) + assert await eiger_writer.get_indices_written() == 2 + @pytest.mark.asyncio -async def test_eiger_writer_observe_indices_written(eiger_writer, mock_eiger_driver): +async def test_eiger_writer_observe_indices_written(eiger_writer: EigerWriter, mock_eiger_driver: EigerDriverIO) -> None: """Test observing indices as they are written.""" - # Initialize the writer first - with patch("pathlib.Path.mkdir"), patch("pathlib.Path.exists", return_value=False): - await eiger_writer.open() - - # Create an async generator to track yielded indices - observed_indices = [] - - async def simulate_writing(): - """Simulate the detector writing images by incrementing sequence_id.""" - await asyncio.sleep(0.05) # Small delay - set_mock_value(mock_eiger_driver.sequence_id, 1.0) - await asyncio.sleep(0.05) - set_mock_value(mock_eiger_driver.sequence_id, 2.0) - await asyncio.sleep(0.05) - set_mock_value(mock_eiger_driver.sequence_id, 3.0) - - # Start the simulation task - sim_task = asyncio.create_task(simulate_writing()) - - # Observe the indices being written - observe_task = asyncio.create_task( - _collect_observed_indices(eiger_writer, observed_indices, max_indices=3) - ) - - # Wait for both tasks to complete - await asyncio.gather(sim_task, observe_task) - - # Should have observed indices 0, 1, 2 - assert observed_indices == [0, 1, 2] - + set_mock_value(mock_eiger_driver.sequence_id, 1) + set_mock_value(mock_eiger_driver.fw_nimgs_per_file, 1) -async def _collect_observed_indices(writer, indices_list, max_indices): - """Helper function to collect observed indices.""" - count = 0 - async for index in writer.observe_indices_written(timeout=1.0): - indices_list.append(index) - count += 1 - if count >= max_indices: - break + async def _simulate_writing_indices(num_images: int, num_triggers: int, acquire_time: float = 0.01) -> list[int]: + # Create an async generator to track yielded indices + observed_indices = [] + set_mock_value(mock_eiger_driver.num_images_counter, 0) + + async def _simulate_acquisition(): + """Simulate the detector writing images by incrementing sequence_id.""" + for i in range(1, num_images * num_triggers + 1): + await asyncio.sleep(acquire_time) + set_mock_value(mock_eiger_driver.num_images_counter, i) + + async def _complete(): + """Helper function to collect observed indices.""" + indices_written = eiger_writer.observe_indices_written(timeout=1.0) + async for index in indices_written: + observed_indices.append(index) + if index >= num_triggers: + break + + # Start the simulation task + sim_task = asyncio.create_task(_simulate_acquisition()) + + # Observe the indices being written + observe_task = asyncio.create_task(_complete()) + + # Wait for both tasks to complete + await asyncio.gather(sim_task, observe_task) + + return observed_indices + # Case 1: 1 image, 1 trigger + set_mock_value(mock_eiger_driver.num_images, 1) + set_mock_value(mock_eiger_driver.num_triggers, 1) + num_images = await mock_eiger_driver.num_images.get_value() + num_triggers = await mock_eiger_driver.num_triggers.get_value() + await eiger_writer.open(name="test_eiger", exposures_per_event=num_images) + observed = await _simulate_writing_indices(num_images=num_images, num_triggers=num_triggers) + assert observed == [0, 1] -@pytest.mark.asyncio -async def test_eiger_writer_collect_stream_docs(eiger_writer, mock_eiger_driver): - """Test collecting stream documents.""" - # Initialize the writer first - with patch("pathlib.Path.mkdir"), patch("pathlib.Path.exists", return_value=False): - await eiger_writer.open() - - # Collect stream docs for 2 indices - docs = [] - async for doc_type, doc in eiger_writer.collect_stream_docs(indices_written=2): - docs.append((doc_type, doc)) - - # Should have 1 stream_resource + 2 stream_datum docs - assert len(docs) == 3 - - # Check stream_resource document - assert docs[0][0] == "stream_resource" - stream_resource = docs[0][1] - assert stream_resource["spec"] == "AD_EIGER" - assert stream_resource["root"] == "/tmp/test_data" - assert "resource_path" in stream_resource - assert stream_resource["resource_kwargs"]["images_per_file"] == 1000 - - # Check stream_datum documents - for i in range(1, 3): - assert docs[i][0] == "stream_datum" - stream_datum = docs[i][1] - assert stream_datum["descriptor"] == "primary" - assert stream_datum["stream_resource"] == stream_resource["uid"] - assert stream_datum["seq_num"] == i - 1 - assert "datum_kwargs" in stream_datum - assert "seq_id" in stream_datum["datum_kwargs"] + # Case 2: 1 image, 5 triggers + set_mock_value(mock_eiger_driver.num_images, 1) + set_mock_value(mock_eiger_driver.num_triggers, 5) + num_images = await mock_eiger_driver.num_images.get_value() + num_triggers = await mock_eiger_driver.num_triggers.get_value() + await eiger_writer.open(name="test_eiger", exposures_per_event=num_images) + observed = await _simulate_writing_indices(num_images=num_images, num_triggers=num_triggers) + assert observed == [0, 1, 2, 3, 4, 5] + + # Case 3: 5 images, 2 triggers + set_mock_value(mock_eiger_driver.num_images, 5) + set_mock_value(mock_eiger_driver.num_triggers, 2) + num_images = await mock_eiger_driver.num_images.get_value() + num_triggers = await mock_eiger_driver.num_triggers.get_value() + await eiger_writer.open(name="test_eiger", exposures_per_event=num_images) + observed = await _simulate_writing_indices(num_images=num_images, num_triggers=num_triggers) + assert observed == [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2] @pytest.mark.asyncio -async def test_eiger_writer_close(eiger_writer, mock_eiger_driver): - """Test closing the writer.""" - # Initialize the writer first - with patch("pathlib.Path.mkdir"), patch("pathlib.Path.exists", return_value=False): - await eiger_writer.open() - - # Mock file existence check - with patch("pathlib.Path.exists", return_value=True): - await eiger_writer.close() - - # Verify the detector was disabled - assert await mock_eiger_driver.fw_enable.get_value() is False - assert await mock_eiger_driver.save_files.get_value() is False - +async def test_eiger_writer_collect_stream_docs(eiger_writer: EigerWriter, mock_eiger_driver: EigerDriverIO) -> None: + """Test collecting stream documents.""" -@pytest.mark.asyncio -async def test_eiger_writer_data_type_mapping(eiger_writer, mock_eiger_driver): - """Test that different data types are mapped correctly.""" - data_type_tests = [ - ("UInt8", "uint8"), - ("UInt16", "uint16"), - ("UInt32", "uint32"), - ("Int8", "int8"), - ("Int16", "int16"), - ("Int32", "int32"), - ("Float32", "float32"), - ("Float64", "float64"), - ("Unknown", "uint16"), # Should default to uint16 - ] - - for eiger_type, expected_numpy_type in data_type_tests: - set_mock_value(mock_eiger_driver.data_type, eiger_type) - - with patch("pathlib.Path.mkdir"), patch("pathlib.Path.exists", return_value=False): - describe_doc = await eiger_writer.open() - - assert describe_doc["primary"]["dtype"] == expected_numpy_type + async def collect_docs(num_triggers: int): + resource_docs = [] + data_docs = [] + for i in range(1, num_triggers + 1): + async for doc_type, doc in eiger_writer.collect_stream_docs(name="test_eiger", indices_written=i): + if doc_type == "stream_resource": + resource_docs.append(doc) + elif doc_type == "stream_datum": + data_docs.append(doc) + return resource_docs, data_docs + + set_mock_value(mock_eiger_driver.sequence_id, 1) + set_mock_value(mock_eiger_driver.fw_nimgs_per_file, 1) + set_mock_value(mock_eiger_driver.num_images, 1) + set_mock_value(mock_eiger_driver.num_triggers, 1) + await eiger_writer.open(name="test_eiger", exposures_per_event=1) + resource_docs, data_docs = await collect_docs(num_triggers=await mock_eiger_driver.num_triggers.get_value()) + assert len(resource_docs) == 10 + assert len(data_docs) == 10 + assert resource_docs[0]["uri"] == "file://localhost/tmp/test_data/test_eiger_1_master.h5" + await eiger_writer.close() -@pytest.mark.asyncio -async def test_eiger_writer_file_validation_warning(eiger_writer, mock_eiger_driver, caplog): - """Test that missing files generate warnings.""" - # Initialize the writer first - with patch("pathlib.Path.mkdir"), patch("pathlib.Path.exists", return_value=False): - await eiger_writer.open() - - # Simulate some images being written - set_mock_value(mock_eiger_driver.sequence_id, 2.0) - - # Mock file existence check to return False (files don't exist) - with patch("pathlib.Path.exists", return_value=False): - await eiger_writer.close() - - # Check that a warning was logged about missing files - assert "Master files were not written" in caplog.text + set_mock_value(mock_eiger_driver.num_triggers, 3) + set_mock_value(mock_eiger_driver.sequence_id, 2) + await eiger_writer.open(name="test_eiger", exposures_per_event=1) + resource_docs, data_docs = await collect_docs(num_triggers=await mock_eiger_driver.num_triggers.get_value()) + assert len(resource_docs) == 12 + assert len(data_docs) == 36 + assert resource_docs[0]["uri"] == "file://localhost/tmp/test_data/test_eiger_2_master.h5" @pytest.mark.asyncio -async def test_eiger_writer_resource_uid_generation(eiger_writer): - """Test that resource UIDs are generated consistently.""" - with patch("pathlib.Path.mkdir"), patch("pathlib.Path.exists", return_value=False): - describe_doc1 = await eiger_writer.open() - - # Resource UID should be set and be 8 characters long - assert eiger_writer._resource_uid is not None - assert len(eiger_writer._resource_uid) == 8 - - # Source should include the resource UID - assert eiger_writer._resource_uid in describe_doc1["primary"]["source"] - +async def test_eiger_writer_close(eiger_writer: EigerWriter, mock_eiger_driver: EigerDriverIO) -> None: + """Test closing the writer.""" -@pytest.mark.asyncio -async def test_eiger_writer_path_creation(eiger_writer): - """Test that write paths are created correctly.""" - with patch("pathlib.Path.mkdir") as mock_mkdir, \ - patch("pathlib.Path.exists", return_value=False): - - # Mock datetime to have consistent path - with patch("cditools.eiger_async.datetime") as mock_datetime: - mock_datetime.now.return_value = datetime(2023, 12, 25, 10, 30, 0) - mock_datetime.strftime = datetime.strftime - - await eiger_writer.open() - - # Check that the path was created - mock_mkdir.assert_called_once() - expected_path = Path("/tmp/test_data/2023/12/25") - assert eiger_writer._write_path == expected_path \ No newline at end of file + # Verify the writing was enabled + set_mock_value(mock_eiger_driver.sequence_id, 1) + set_mock_value(mock_eiger_driver.num_images, 1) + set_mock_value(mock_eiger_driver.num_triggers, 1) + set_mock_value(mock_eiger_driver.fw_nimgs_per_file, 1) + await eiger_writer.open(name="test_eiger", exposures_per_event=1) + assert await mock_eiger_driver.fw_enable.get_value() is True + assert await mock_eiger_driver.save_files.get_value() is True + + # Verify the writing was disabled + await eiger_writer.close() + assert await mock_eiger_driver.fw_enable.get_value() is False + assert await mock_eiger_driver.save_files.get_value() is False + assert eiger_writer._composer is None + assert eiger_writer._current_sequence_id is None + assert eiger_writer._file_info is None From 89ef73b8514c81afbe74ac98ad5167f9c965dad6 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Tue, 1 Jul 2025 16:54:47 -0400 Subject: [PATCH 07/46] Add TODO reminder --- tests/test_eiger_async.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_eiger_async.py b/tests/test_eiger_async.py index 4d2bfdb2..31ea71aa 100644 --- a/tests/test_eiger_async.py +++ b/tests/test_eiger_async.py @@ -313,3 +313,6 @@ async def test_eiger_writer_close(eiger_writer: EigerWriter, mock_eiger_driver: assert eiger_writer._composer is None assert eiger_writer._current_sequence_id is None assert eiger_writer._file_info is None + + +# TODO: Test the controller's overridden methods and do an integration test with bluesky plans + tiled readback \ No newline at end of file From 6bbaadb8309089c45c6b9d8001beb47504e8126a Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Wed, 2 Jul 2025 09:19:39 -0400 Subject: [PATCH 08/46] pre-commit --- src/cditools/eiger_async.py | 153 ++++++++++++++++++++--------- tests/conftest.py | 4 +- tests/test_eiger_async.py | 187 ++++++++++++++++++++++-------------- 3 files changed, 228 insertions(+), 116 deletions(-) diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index ee5231a6..17d198fd 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -1,24 +1,42 @@ """ Ophyd Async implementation for Eiger detector. """ + +from __future__ import annotations + import asyncio +from collections.abc import AsyncGenerator, AsyncIterator, Sequence +from logging import getLogger from math import ceil -from typing import Sequence, Annotated as A, Any from pathlib import Path -from logging import getLogger -from collections.abc import AsyncGenerator, AsyncIterator +from typing import Annotated as A +from typing import Any -from bluesky.protocols import StreamAsset import numpy as np +from bluesky.protocols import StreamAsset from event_model import DataKey from ophyd_async.core import ( - PathProvider, TriggerInfo, SignalRW, SignalR, DetectorTrigger, - DatasetDescriber, DetectorWriter, - HDFDatasetDescription, HDFDocumentComposer, observe_value, PathInfo + DatasetDescriber, + DetectorTrigger, + DetectorWriter, + HDFDatasetDescription, + HDFDocumentComposer, + PathInfo, + PathProvider, + SignalR, + SignalRW, + StrictEnum, + TriggerInfo, + observe_value, +) +from ophyd_async.epics.adcore import ( + ADBaseController, + ADBaseIO, + ADImageMode, + AreaDetector, + NDPluginBaseIO, ) from ophyd_async.epics.signal import PvSuffix -from ophyd_async.epics.adcore import NDPluginBaseIO, ADBaseIO, AreaDetector, ADBaseController, ADImageMode -from ophyd_async.core import StrictEnum logger = getLogger(__name__) @@ -28,6 +46,7 @@ class EigerTriggerMode(StrictEnum): See https://areadetector.github.io/areaDetector/ADEiger/eiger.html#implementation-of-standard-driver-parameters """ + INTERNAL_SERIES = "ints" INTERNAL_ENABLE = "int" EXTERNAL_SERIES = "exts" @@ -40,6 +59,7 @@ class EigerExtGateMode(StrictEnum): See https://areadetector.github.io/areaDetector/ADEiger/eiger.html#trigger-setup """ + PUMP_AND_PROBE = "Pump & Probe" HDR = "HDR" @@ -47,8 +67,9 @@ class EigerExtGateMode(StrictEnum): class EigerROIMode(StrictEnum): """ROI modes for the Eiger detector. - See https://areadetector.github.io/areaDetector/ADEiger/eiger.html#readout-setup + See https://areadetector.github.io/areaDetector/ADEiger/eiger.html#readout-setup """ + DISABLED = "Disabled" _4M = "4M" @@ -56,8 +77,9 @@ class EigerROIMode(StrictEnum): class EigerCompressionAlgo(StrictEnum): """Compression algorithms for the Eiger detector. - See https://areadetector.github.io/areaDetector/ADEiger/eiger.html#readout-setup + See https://areadetector.github.io/areaDetector/ADEiger/eiger.html#readout-setup """ + LZ4 = "lz4" BSLZ4 = "bslz4" NONE = "None" @@ -68,6 +90,7 @@ class EigerDataSource(StrictEnum): See https://areadetector.github.io/areaDetector/ADEiger/eiger.html#readout-setup """ + NONE = "None" FILE_WRITER = "FileWriter" STREAM = "Stream" @@ -78,6 +101,7 @@ class EigerHDF5Format(StrictEnum): See https://areadetector.github.io/areaDetector/ADEiger/eiger.html#filewriter-interface """ + LEGACY = "Legacy" V2024_2 = "v2024.2" @@ -85,8 +109,9 @@ class EigerHDF5Format(StrictEnum): class EigerStreamVersion(StrictEnum): """Stream versions for the Eiger detector. - See https://areadetector.github.io/areaDetector/ADEiger/eiger.html#stream-interface + See https://areadetector.github.io/areaDetector/ADEiger/eiger.html#stream-interface """ + STREAM1 = "Stream1" STREAM2 = "Stream2" @@ -96,6 +121,7 @@ class EigerStreamHdrDetail(StrictEnum): See https://areadetector.github.io/areaDetector/ADEiger/eiger.html#stream-interface """ + ALL = "All" BASIC = "Basic" NONE = "None" @@ -106,6 +132,7 @@ class EigerDriverIO(ADBaseIO): See https://areadetector.github.io/areaDetector/ADEiger/eiger.html#implementation-of-standard-driver-parameters """ + # Standard Driver Parameters trigger_mode: A[SignalRW[EigerTriggerMode], PvSuffix.rbv("TriggerMode")] num_images: A[SignalRW[int], PvSuffix.rbv("NumImages")] @@ -156,7 +183,7 @@ class EigerDriverIO(ADBaseIO): threshold_diff_enable: A[SignalRW[bool], PvSuffix.rbv("ThresholdDiffEnable")] photon_energy: A[SignalRW[float], PvSuffix.rbv("PhotonEnergy")] counting_mode: A[SignalRW[str], PvSuffix.rbv("CountingMode")] - + # Trigger Setup ext_gate_mode: A[SignalRW[str], PvSuffix.rbv("ExtGateMode")] trigger: A[SignalRW[float], PvSuffix("Trigger")] @@ -174,12 +201,12 @@ class EigerDriverIO(ADBaseIO): signed_data: A[SignalRW[bool], PvSuffix.rbv("SignedData")] compression_algo: A[SignalRW[EigerCompressionAlgo], PvSuffix.rbv("CompressionAlgo")] data_source: A[SignalRW[EigerDataSource], PvSuffix.rbv("DataSource")] - + # Acquisition Status armed: A[SignalR[bool], PvSuffix("Armed")] bit_depth_image: A[SignalR[int], PvSuffix("BitDepthImage_RBV")] count_cutoff: A[SignalR[float], PvSuffix("CountCutoff_RBV")] - + # FileWriter Interface fw_enable: A[SignalRW[bool], PvSuffix.rbv("FWEnable")] fw_state: A[SignalR[str], PvSuffix("FWState_RBV")] @@ -196,17 +223,19 @@ class EigerDriverIO(ADBaseIO): fw_free: A[SignalR[float], PvSuffix("FWFree_RBV")] fw_auto_remove: A[SignalRW[bool], PvSuffix.rbv("FWAutoRemove")] fw_clear: A[SignalRW[float], PvSuffix("FWClear")] - + # Stream Interface stream_enable: A[SignalRW[bool], PvSuffix.rbv("StreamEnable")] stream_state: A[SignalR[str], PvSuffix("StreamState_RBV")] stream_version: A[SignalRW[EigerStreamVersion], PvSuffix.rbv("StreamVersion")] stream_decompress: A[SignalRW[bool], PvSuffix.rbv("StreamDecompress")] - stream_hdr_detail: A[SignalRW[EigerStreamHdrDetail], PvSuffix.rbv("StreamHdrDetail")] + stream_hdr_detail: A[ + SignalRW[EigerStreamHdrDetail], PvSuffix.rbv("StreamHdrDetail") + ] stream_hdr_appendix: A[SignalRW[str], PvSuffix.rbv("StreamHdrAppendix")] stream_img_appendix: A[SignalRW[str], PvSuffix.rbv("StreamImgAppendix")] stream_dropped: A[SignalR[float], PvSuffix("StreamDropped_RBV")] - + # Monitor Interface monitor_enable: A[SignalRW[bool], PvSuffix.rbv("MonitorEnable")] monitor_state: A[SignalR[str], PvSuffix("MonitorState_RBV")] @@ -217,7 +246,7 @@ class EigerDriverIO(ADBaseIO): beam_y: A[SignalRW[float], PvSuffix.rbv("BeamY")] det_dist: A[SignalRW[float], PvSuffix.rbv("DetDist")] wavelength: A[SignalRW[float], PvSuffix.rbv("Wavelength")] - + # Detector Metadata chi_start: A[SignalRW[float], PvSuffix.rbv("ChiStart")] chi_incr: A[SignalRW[float], PvSuffix.rbv("ChiIncr")] @@ -229,7 +258,7 @@ class EigerDriverIO(ADBaseIO): phi_incr: A[SignalRW[float], PvSuffix.rbv("PhiIncr")] two_theta_start: A[SignalRW[float], PvSuffix.rbv("TwoThetaStart")] two_theta_incr: A[SignalRW[float], PvSuffix.rbv("TwoThetaIncr")] - + # Minimum change allowed wavelength_eps: A[SignalRW[float], PvSuffix.rbv("WavelengthEps")] energy_eps: A[SignalRW[float], PvSuffix.rbv("EnergyEps")] @@ -238,7 +267,12 @@ class EigerDriverIO(ADBaseIO): class EigerWriter(DetectorWriter): """Eiger-specific file writer using the built-in FileWriter interface.""" - def __init__(self, driver: EigerDriverIO, path_provider: PathProvider, dataset_describer: DatasetDescriber): + def __init__( + self, + driver: EigerDriverIO, + path_provider: PathProvider, + dataset_describer: DatasetDescriber, + ): self._driver = driver self._path_provider = path_provider self._dataset_describer = dataset_describer @@ -256,7 +290,7 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK # Set the name pattern with $id replacement similar to original name_pattern = f"{self._file_info.filename}_$id" - + # Configure the Eiger FileWriter await asyncio.gather( self._driver.file_path.set(self._file_info.directory_path.as_posix()), @@ -266,7 +300,7 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK self._driver.fw_hdf5_format.set(EigerHDF5Format.LEGACY), self._driver.data_source.set(EigerDataSource.FILE_WRITER), ) - + # Get important values from the detector num_images, num_images_per_file, num_triggers = await asyncio.gather( self._driver.num_images.get_value(), @@ -277,9 +311,11 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK # Exposures per event is a combination of multiple signals, so we can't simply # set it on the detector, unlike other detector implementations if num_images != exposures_per_event: - msg = ("Mismatch between the number of images set in the detector " - "and the expected number of exposures per event. " - f"Got {num_images} but expected {exposures_per_event}.") + msg = ( + "Mismatch between the number of images set in the detector " + "and the expected number of exposures per event. " + f"Got {num_images} but expected {exposures_per_event}." + ) raise ValueError(msg) detector_shape, np_dtype = await asyncio.gather( @@ -371,7 +407,13 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK HDFDatasetDescription( data_key=f"{name}_{i}", dataset=f"/entry/data_{i:06d}", - shape=(min(num_images_per_file, total_images - (i - 1) * num_images_per_file), *detector_shape), + shape=( + min( + num_images_per_file, + total_images - (i - 1) * num_images_per_file, + ), + *detector_shape, + ), dtype_numpy=np_dtype, chunk_shape=(1, *detector_shape), ) @@ -384,7 +426,9 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK ds.data_key: DataKey( source=self._master_file_path.as_posix(), shape=list(ds.shape), - dtype="array" if exposures_per_event > 1 or len(ds.shape) > 1 else "number", + dtype="array" + if exposures_per_event > 1 or len(ds.shape) > 1 + else "number", dtype_numpy=ds.dtype_numpy, external="STREAM:", ) @@ -396,17 +440,28 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK @property def _master_file_path(self) -> Path: if self._current_sequence_id is None or self._file_info is None: - raise ValueError("Must call EigerWriter.open() before accessing master file path") - return self._file_info.directory_path / f"{self._file_info.filename}_{self._current_sequence_id}_master.h5" + raise ValueError( + "Must call EigerWriter.open() before accessing master file path" + ) + return ( + self._file_info.directory_path + / f"{self._file_info.filename}_{self._current_sequence_id}_master.h5" + ) async def compute_index(self, num_images_counter: int) -> int: if await self._driver.num_images.get_value() != self._exposures_per_event: - raise RuntimeError("Detected change to the number of images during acquisition. This is not allowed.") + raise RuntimeError( + "Detected change to the number of images during acquisition. This is not allowed." + ) return num_images_counter // self._exposures_per_event - async def observe_indices_written(self, timeout: float = 10.0) -> AsyncGenerator[int, None]: + async def observe_indices_written( + self, timeout: float = 10.0 + ) -> AsyncGenerator[int, None]: """Monitor the number of files written by the Eiger FileWriter.""" - async for num_images_counter in observe_value(self._driver.num_images_counter, timeout): + async for num_images_counter in observe_value( + self._driver.num_images_counter, timeout + ): print(f"{num_images_counter=}") yield await self.compute_index(num_images_counter) @@ -417,9 +472,13 @@ async def get_indices_written(self) -> int: to correspond to a single index. This is in contrast to the ophyd-sync implementation, where the `num_triggers` was ignored. """ - return await self.compute_index(await self._driver.num_images_counter.get_value()) + return await self.compute_index( + await self._driver.num_images_counter.get_value() + ) - async def collect_stream_docs(self, name: str, indices_written: int) -> AsyncIterator[StreamAsset]: + async def collect_stream_docs( + self, name: str, indices_written: int + ) -> AsyncIterator[StreamAsset]: """Generate stream documents for the written HDF5 files.""" if indices_written: if not self._composer: @@ -452,8 +511,10 @@ async def close(self) -> None: class EigerController(ADBaseController[EigerDriverIO]): """Controller for Eiger detector, handling trigger modes and acquisition setup.""" - - def __init__(self, driver: EigerDriverIO, *args: Any, **kwargs: dict[str, Any]) -> None: + + def __init__( + self, driver: EigerDriverIO, *args: Any, **kwargs: dict[str, Any] + ) -> None: super().__init__(driver, *args, **kwargs) def get_deadtime(self, exposure: float | None) -> float: @@ -470,10 +531,15 @@ async def prepare(self, trigger_info: TriggerInfo) -> None: await self._driver.trigger_mode.set(EigerTriggerMode.INTERNAL_SERIES) elif trigger_info.trigger == DetectorTrigger.EXTERNAL: await self._driver.trigger_mode.set(EigerTriggerMode.EXTERNAL_SERIES) - elif trigger_info.trigger in [DetectorTrigger.VARIABLE_GATE, DetectorTrigger.CONSTANT_GATE]: + elif trigger_info.trigger in [ + DetectorTrigger.VARIABLE_GATE, + DetectorTrigger.CONSTANT_GATE, + ]: await self._driver.trigger_mode.set(EigerTriggerMode.EXTERNAL_GATE) else: - raise NotImplementedError(f"Trigger mode {trigger_info.trigger} not supported") + raise NotImplementedError( + f"Trigger mode {trigger_info.trigger} not supported" + ) if trigger_info.total_number_of_exposures == 0: image_mode = ADImageMode.CONTINUOUS @@ -503,7 +569,7 @@ def __init__( ): driver = EigerDriverIO(prefix + driver_suffix) controller = EigerController(driver) - + # If the writer class is an ADWriter, use the with_io method # since we want to use one of the AD plugins. # Otherwise, use the internal file writer. @@ -517,9 +583,11 @@ def __init__( ) else: if fileio_suffix is not None or plugins is not None: - logger.warning("Ignoring params fileio_suffix and plugins for non-ADWriter writer") + logger.warning( + "Ignoring params fileio_suffix and plugins for non-ADWriter writer" + ) writer = writer_cls(driver, path_provider) - + super().__init__( controller=controller, writer=writer, @@ -527,4 +595,3 @@ def __init__( name=name, config_sigs=config_sigs, ) - \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 8eb4f743..ca80f278 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,12 +1,14 @@ +from __future__ import annotations + import asyncio import pprint import pytest from bluesky.run_engine import RunEngine, TransitionError - _ALLOWED_PYTEST_TASKS = {"async_finalizer", "async_setup", "async_teardown"} + def _error_and_kill_pending_tasks( loop: asyncio.AbstractEventLoop, test_name: str, test_passed: bool ) -> set[asyncio.Task]: diff --git a/tests/test_eiger_async.py b/tests/test_eiger_async.py index 31ea71aa..6275a6e4 100644 --- a/tests/test_eiger_async.py +++ b/tests/test_eiger_async.py @@ -1,33 +1,30 @@ """ Tests for the EigerWriter class using ophyd-async mocking utilities. """ + +from __future__ import annotations + import asyncio -from datetime import datetime -from pathlib import Path -from unittest.mock import patch import numpy as np -from bluesky.run_engine import RunEngine import pytest +from bluesky.run_engine import RunEngine from ophyd_async.core import ( PathProvider, - init_devices, - PathInfo, - StaticPathProvider, StaticFilenameProvider, + StaticPathProvider, + init_devices, ) from ophyd_async.epics.adcore import ADBaseDatasetDescriber from ophyd_async.testing import ( set_mock_value, - assert_reading, - assert_value, ) from cditools.eiger_async import ( - EigerDriverIO, - EigerWriter, EigerDataSource, + EigerDriverIO, EigerHDF5Format, + EigerWriter, ) @@ -36,12 +33,12 @@ def mock_eiger_driver(RE: RunEngine) -> EigerDriverIO: """Create a mock EigerDriverIO for testing.""" with init_devices(mock=True): driver = EigerDriverIO("MOCK:EIGER:CAM:") - + # Set up some default mock values set_mock_value(driver.array_size_x, 2048) set_mock_value(driver.array_size_y, 2048) set_mock_value(driver.data_type, "UInt16") - + return driver @@ -49,20 +46,25 @@ def mock_eiger_driver(RE: RunEngine) -> EigerDriverIO: def mock_path_provider() -> PathProvider: """Create a mock path provider for testing.""" return StaticPathProvider( - StaticFilenameProvider("test_eiger"), - directory_path="/tmp/test_data" + StaticFilenameProvider("test_eiger"), directory_path="/tmp/test_data" ) @pytest.fixture -def eiger_writer(mock_eiger_driver: EigerDriverIO, mock_path_provider: PathProvider) -> EigerWriter: +def eiger_writer( + mock_eiger_driver: EigerDriverIO, mock_path_provider: PathProvider +) -> EigerWriter: """Create an EigerWriter instance for testing.""" dataset_describer = ADBaseDatasetDescriber(mock_eiger_driver) return EigerWriter(mock_eiger_driver, mock_path_provider, dataset_describer) @pytest.mark.asyncio -async def test_eiger_writer_initialization(eiger_writer: EigerWriter, mock_eiger_driver: EigerDriverIO, mock_path_provider: PathProvider): +async def test_eiger_writer_initialization( + eiger_writer: EigerWriter, + mock_eiger_driver: EigerDriverIO, + mock_path_provider: PathProvider, +): """Test that EigerWriter initializes correctly.""" assert eiger_writer._driver is mock_eiger_driver assert eiger_writer._path_provider is mock_path_provider @@ -73,7 +75,9 @@ async def test_eiger_writer_initialization(eiger_writer: EigerWriter, mock_eiger @pytest.mark.asyncio -async def test_eiger_writer_open(eiger_writer: EigerWriter, mock_eiger_driver: EigerDriverIO) -> None: +async def test_eiger_writer_open( + eiger_writer: EigerWriter, mock_eiger_driver: EigerDriverIO +) -> None: """Test the open method configures the detector correctly.""" array_size_x, array_size_y, data_type = await asyncio.gather( mock_eiger_driver.array_size_x.get_value(), @@ -91,18 +95,24 @@ async def test_eiger_writer_open(eiger_writer: EigerWriter, mock_eiger_driver: E assert await mock_eiger_driver.fw_enable.get_value() is True assert await mock_eiger_driver.save_files.get_value() is True assert await mock_eiger_driver.fw_hdf5_format.get_value() == EigerHDF5Format.LEGACY - assert await mock_eiger_driver.data_source.get_value() == EigerDataSource.FILE_WRITER - assert description.keys() == {"test_eiger_y_pixel_size", - "test_eiger_x_pixel_size", - "test_eiger_detector_distance", - "test_eiger_incident_wavelength", - "test_eiger_frame_time", - "test_eiger_beam_center_x", - "test_eiger_beam_center_y", - "test_eiger_count_time", - "test_eiger_pixel_mask", - "test_eiger_1"} - assert description["test_eiger_1"]["source"] == "/tmp/test_data/test_eiger_1_master.h5" + assert ( + await mock_eiger_driver.data_source.get_value() == EigerDataSource.FILE_WRITER + ) + assert description.keys() == { + "test_eiger_y_pixel_size", + "test_eiger_x_pixel_size", + "test_eiger_detector_distance", + "test_eiger_incident_wavelength", + "test_eiger_frame_time", + "test_eiger_beam_center_x", + "test_eiger_beam_center_y", + "test_eiger_count_time", + "test_eiger_pixel_mask", + "test_eiger_1", + } + assert ( + description["test_eiger_1"]["source"] == "/tmp/test_data/test_eiger_1_master.h5" + ) # Case 2: 4 images per file, 11 images, 2 triggers # Expect 6 files, the first 5 will have 4 images, the last will have 2 @@ -110,22 +120,27 @@ async def test_eiger_writer_open(eiger_writer: EigerWriter, mock_eiger_driver: E set_mock_value(mock_eiger_driver.fw_nimgs_per_file, 4) set_mock_value(mock_eiger_driver.num_images, 11) set_mock_value(mock_eiger_driver.num_triggers, 2) - description = await eiger_writer.open(name="test_eiger", exposures_per_event=await mock_eiger_driver.num_images.get_value()) - assert description.keys() == {"test_eiger_y_pixel_size", - "test_eiger_x_pixel_size", - "test_eiger_detector_distance", - "test_eiger_incident_wavelength", - "test_eiger_frame_time", - "test_eiger_beam_center_x", - "test_eiger_beam_center_y", - "test_eiger_count_time", - "test_eiger_pixel_mask", - "test_eiger_1", - "test_eiger_2", - "test_eiger_3", - "test_eiger_4", - "test_eiger_5", - "test_eiger_6"} + description = await eiger_writer.open( + name="test_eiger", + exposures_per_event=await mock_eiger_driver.num_images.get_value(), + ) + assert description.keys() == { + "test_eiger_y_pixel_size", + "test_eiger_x_pixel_size", + "test_eiger_detector_distance", + "test_eiger_incident_wavelength", + "test_eiger_frame_time", + "test_eiger_beam_center_x", + "test_eiger_beam_center_y", + "test_eiger_count_time", + "test_eiger_pixel_mask", + "test_eiger_1", + "test_eiger_2", + "test_eiger_3", + "test_eiger_4", + "test_eiger_5", + "test_eiger_6", + } for i in range(1, 6): data_key = description[f"test_eiger_{i}"] @@ -133,13 +148,13 @@ async def test_eiger_writer_open(eiger_writer: EigerWriter, mock_eiger_driver: E assert data_key["dtype"] == "array" assert data_key["dtype_numpy"] == np.dtype(data_type.lower()).str assert data_key["external"] == "STREAM:" - assert data_key["source"] == f"/tmp/test_data/test_eiger_2_master.h5" + assert data_key["source"] == "/tmp/test_data/test_eiger_2_master.h5" data_key = description["test_eiger_6"] assert tuple(data_key["shape"]) == (2, array_size_x, array_size_y) assert data_key["dtype"] == "array" assert data_key["dtype_numpy"] == np.dtype(data_type.lower()).str assert data_key["external"] == "STREAM:" - assert data_key["source"] == f"/tmp/test_data/test_eiger_2_master.h5" + assert data_key["source"] == "/tmp/test_data/test_eiger_2_master.h5" @pytest.mark.asyncio @@ -154,7 +169,8 @@ async def test_eiger_writer_get_indices_written(eiger_writer, mock_eiger_driver) set_mock_value(mock_eiger_driver.num_images_counter, 0) await eiger_writer.open( name="test_eiger", - exposures_per_event=await mock_eiger_driver.num_images.get_value()) + exposures_per_event=await mock_eiger_driver.num_images.get_value(), + ) assert await eiger_writer.get_indices_written() == 0 set_mock_value(mock_eiger_driver.num_images_counter, 1) assert await eiger_writer.get_indices_written() == 1 @@ -165,7 +181,8 @@ async def test_eiger_writer_get_indices_written(eiger_writer, mock_eiger_driver) set_mock_value(mock_eiger_driver.num_images_counter, 0) await eiger_writer.open( name="test_eiger", - exposures_per_event=await mock_eiger_driver.num_images.get_value()) + exposures_per_event=await mock_eiger_driver.num_images.get_value(), + ) assert await eiger_writer.get_indices_written() == 0 set_mock_value(mock_eiger_driver.num_images_counter, 1) assert await eiger_writer.get_indices_written() == 1 @@ -180,10 +197,11 @@ async def test_eiger_writer_get_indices_written(eiger_writer, mock_eiger_driver) set_mock_value(mock_eiger_driver.num_images_counter, 0) await eiger_writer.open( name="test_eiger", - exposures_per_event=await mock_eiger_driver.num_images.get_value()) + exposures_per_event=await mock_eiger_driver.num_images.get_value(), + ) assert await eiger_writer.get_indices_written() == 0 set_mock_value(mock_eiger_driver.num_images_counter, 4) - assert await eiger_writer.get_indices_written() == 0 + assert await eiger_writer.get_indices_written() == 0 set_mock_value(mock_eiger_driver.num_images_counter, 5) assert await eiger_writer.get_indices_written() == 1 set_mock_value(mock_eiger_driver.num_images_counter, 9) @@ -192,18 +210,21 @@ async def test_eiger_writer_get_indices_written(eiger_writer, mock_eiger_driver) assert await eiger_writer.get_indices_written() == 2 - @pytest.mark.asyncio -async def test_eiger_writer_observe_indices_written(eiger_writer: EigerWriter, mock_eiger_driver: EigerDriverIO) -> None: +async def test_eiger_writer_observe_indices_written( + eiger_writer: EigerWriter, mock_eiger_driver: EigerDriverIO +) -> None: """Test observing indices as they are written.""" set_mock_value(mock_eiger_driver.sequence_id, 1) set_mock_value(mock_eiger_driver.fw_nimgs_per_file, 1) - async def _simulate_writing_indices(num_images: int, num_triggers: int, acquire_time: float = 0.01) -> list[int]: + async def _simulate_writing_indices( + num_images: int, num_triggers: int, acquire_time: float = 0.01 + ) -> list[int]: # Create an async generator to track yielded indices observed_indices = [] set_mock_value(mock_eiger_driver.num_images_counter, 0) - + async def _simulate_acquisition(): """Simulate the detector writing images by incrementing sequence_id.""" for i in range(1, num_images * num_triggers + 1): @@ -217,16 +238,16 @@ async def _complete(): observed_indices.append(index) if index >= num_triggers: break - + # Start the simulation task sim_task = asyncio.create_task(_simulate_acquisition()) - + # Observe the indices being written observe_task = asyncio.create_task(_complete()) - + # Wait for both tasks to complete await asyncio.gather(sim_task, observe_task) - + return observed_indices # Case 1: 1 image, 1 trigger @@ -235,7 +256,9 @@ async def _complete(): num_images = await mock_eiger_driver.num_images.get_value() num_triggers = await mock_eiger_driver.num_triggers.get_value() await eiger_writer.open(name="test_eiger", exposures_per_event=num_images) - observed = await _simulate_writing_indices(num_images=num_images, num_triggers=num_triggers) + observed = await _simulate_writing_indices( + num_images=num_images, num_triggers=num_triggers + ) assert observed == [0, 1] # Case 2: 1 image, 5 triggers @@ -244,7 +267,9 @@ async def _complete(): num_images = await mock_eiger_driver.num_images.get_value() num_triggers = await mock_eiger_driver.num_triggers.get_value() await eiger_writer.open(name="test_eiger", exposures_per_event=num_images) - observed = await _simulate_writing_indices(num_images=num_images, num_triggers=num_triggers) + observed = await _simulate_writing_indices( + num_images=num_images, num_triggers=num_triggers + ) assert observed == [0, 1, 2, 3, 4, 5] # Case 3: 5 images, 2 triggers @@ -253,19 +278,25 @@ async def _complete(): num_images = await mock_eiger_driver.num_images.get_value() num_triggers = await mock_eiger_driver.num_triggers.get_value() await eiger_writer.open(name="test_eiger", exposures_per_event=num_images) - observed = await _simulate_writing_indices(num_images=num_images, num_triggers=num_triggers) + observed = await _simulate_writing_indices( + num_images=num_images, num_triggers=num_triggers + ) assert observed == [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2] @pytest.mark.asyncio -async def test_eiger_writer_collect_stream_docs(eiger_writer: EigerWriter, mock_eiger_driver: EigerDriverIO) -> None: +async def test_eiger_writer_collect_stream_docs( + eiger_writer: EigerWriter, mock_eiger_driver: EigerDriverIO +) -> None: """Test collecting stream documents.""" async def collect_docs(num_triggers: int): resource_docs = [] data_docs = [] for i in range(1, num_triggers + 1): - async for doc_type, doc in eiger_writer.collect_stream_docs(name="test_eiger", indices_written=i): + async for doc_type, doc in eiger_writer.collect_stream_docs( + name="test_eiger", indices_written=i + ): if doc_type == "stream_resource": resource_docs.append(doc) elif doc_type == "stream_datum": @@ -277,24 +308,36 @@ async def collect_docs(num_triggers: int): set_mock_value(mock_eiger_driver.num_images, 1) set_mock_value(mock_eiger_driver.num_triggers, 1) await eiger_writer.open(name="test_eiger", exposures_per_event=1) - resource_docs, data_docs = await collect_docs(num_triggers=await mock_eiger_driver.num_triggers.get_value()) + resource_docs, data_docs = await collect_docs( + num_triggers=await mock_eiger_driver.num_triggers.get_value() + ) assert len(resource_docs) == 10 assert len(data_docs) == 10 - assert resource_docs[0]["uri"] == "file://localhost/tmp/test_data/test_eiger_1_master.h5" + assert ( + resource_docs[0]["uri"] + == "file://localhost/tmp/test_data/test_eiger_1_master.h5" + ) await eiger_writer.close() set_mock_value(mock_eiger_driver.num_triggers, 3) set_mock_value(mock_eiger_driver.sequence_id, 2) await eiger_writer.open(name="test_eiger", exposures_per_event=1) - resource_docs, data_docs = await collect_docs(num_triggers=await mock_eiger_driver.num_triggers.get_value()) + resource_docs, data_docs = await collect_docs( + num_triggers=await mock_eiger_driver.num_triggers.get_value() + ) assert len(resource_docs) == 12 assert len(data_docs) == 36 - assert resource_docs[0]["uri"] == "file://localhost/tmp/test_data/test_eiger_2_master.h5" + assert ( + resource_docs[0]["uri"] + == "file://localhost/tmp/test_data/test_eiger_2_master.h5" + ) @pytest.mark.asyncio -async def test_eiger_writer_close(eiger_writer: EigerWriter, mock_eiger_driver: EigerDriverIO) -> None: +async def test_eiger_writer_close( + eiger_writer: EigerWriter, mock_eiger_driver: EigerDriverIO +) -> None: """Test closing the writer.""" # Verify the writing was enabled @@ -305,7 +348,7 @@ async def test_eiger_writer_close(eiger_writer: EigerWriter, mock_eiger_driver: await eiger_writer.open(name="test_eiger", exposures_per_event=1) assert await mock_eiger_driver.fw_enable.get_value() is True assert await mock_eiger_driver.save_files.get_value() is True - + # Verify the writing was disabled await eiger_writer.close() assert await mock_eiger_driver.fw_enable.get_value() is False @@ -315,4 +358,4 @@ async def test_eiger_writer_close(eiger_writer: EigerWriter, mock_eiger_driver: assert eiger_writer._file_info is None -# TODO: Test the controller's overridden methods and do an integration test with bluesky plans + tiled readback \ No newline at end of file +# TODO: Test the controller's overridden methods and do an integration test with bluesky plans + tiled readback From eeed7d41948b2b6b82193099e62487e0366287ef Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Wed, 2 Jul 2025 09:36:11 -0400 Subject: [PATCH 09/46] Ruff fixes --- src/cditools/eiger_async.py | 27 +++++++++++---------------- tests/conftest.py | 14 +++++++------- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index 17d198fd..754c4e44 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -33,6 +33,7 @@ ADBaseController, ADBaseIO, ADImageMode, + ADWriter, AreaDetector, NDPluginBaseIO, ) @@ -422,7 +423,7 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK self._datasets = master_datasets + frame_datasets - describe = { + return { ds.data_key: DataKey( source=self._master_file_path.as_posix(), shape=list(ds.shape), @@ -435,14 +436,11 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK for ds in self._datasets } - return describe - @property def _master_file_path(self) -> Path: if self._current_sequence_id is None or self._file_info is None: - raise ValueError( - "Must call EigerWriter.open() before accessing master file path" - ) + msg = "Must call EigerWriter.open() before accessing master file path" + raise ValueError(msg) return ( self._file_info.directory_path / f"{self._file_info.filename}_{self._current_sequence_id}_master.h5" @@ -450,9 +448,8 @@ def _master_file_path(self) -> Path: async def compute_index(self, num_images_counter: int) -> int: if await self._driver.num_images.get_value() != self._exposures_per_event: - raise RuntimeError( - "Detected change to the number of images during acquisition. This is not allowed." - ) + msg = "Detected change to the number of images during acquisition. This is not allowed." + raise RuntimeError(msg) return num_images_counter // self._exposures_per_event async def observe_indices_written( @@ -462,7 +459,6 @@ async def observe_indices_written( async for num_images_counter in observe_value( self._driver.num_images_counter, timeout ): - print(f"{num_images_counter=}") yield await self.compute_index(num_images_counter) async def get_indices_written(self) -> int: @@ -477,7 +473,7 @@ async def get_indices_written(self) -> int: ) async def collect_stream_docs( - self, name: str, indices_written: int + self, _name: str, indices_written: int ) -> AsyncIterator[StreamAsset]: """Generate stream documents for the written HDF5 files.""" if indices_written: @@ -502,7 +498,7 @@ async def close(self) -> None: ) if not self._master_file_path.exists(): - logger.warning(f"Master file was not written: {self._master_file_path}") + logger.warning("Master file was not written: %s", self._master_file_path) self._composer = None self._file_info = None @@ -517,7 +513,7 @@ def __init__( ) -> None: super().__init__(driver, *args, **kwargs) - def get_deadtime(self, exposure: float | None) -> float: + def get_deadtime(self, _exposure: float | None) -> float: """Get detector deadtime for the given exposure.""" return 0.001 @@ -537,9 +533,8 @@ async def prepare(self, trigger_info: TriggerInfo) -> None: ]: await self._driver.trigger_mode.set(EigerTriggerMode.EXTERNAL_GATE) else: - raise NotImplementedError( - f"Trigger mode {trigger_info.trigger} not supported" - ) + msg = f"Trigger mode {trigger_info.trigger} not supported" + raise NotImplementedError(msg) if trigger_info.total_number_of_exposures == 0: image_mode = ADImageMode.CONTINUOUS diff --git a/tests/conftest.py b/tests/conftest.py index ca80f278..2d58239b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +import contextlib import pprint import pytest @@ -41,15 +42,16 @@ def _error_and_kill_pending_tasks( # If it did then it makes sense that there's some tasks we need to cancel, # but an exception will already have been raised. if unfinished_tasks and test_passed: - raise RuntimeError( + msg = ( f"Not all tasks closed during test {test_name}:\n" f"{pprint.pformat(unfinished_tasks, width=88)}" ) + raise RuntimeError(msg) return unfinished_tasks -@pytest.fixture(scope="function") +@pytest.fixture def RE(request: pytest.FixtureRequest): loop = asyncio.new_event_loop() loop.set_debug(True) @@ -58,10 +60,8 @@ def RE(request: pytest.FixtureRequest): def clean_event_loop(): if RE.state not in ("idle", "panicked"): - try: + with contextlib.suppress(TransitionError): RE.halt() - except TransitionError: - pass loop.call_soon_threadsafe(loop.stop) RE._th.join() @@ -73,5 +73,5 @@ def clean_event_loop(): finally: loop.close() - request.addfinalizer(clean_event_loop) - return RE + yield RE + clean_event_loop() From fbfc09f26aebdcfc082552bf4d860de36bcf78b7 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Wed, 2 Jul 2025 10:15:15 -0400 Subject: [PATCH 10/46] Added EigerController tests --- src/cditools/eiger_async.py | 16 ++++----- tests/test_eiger_async.py | 70 +++++++++++++++++++++++++++++++++++-- 2 files changed, 76 insertions(+), 10 deletions(-) diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index 754c4e44..4ad295fa 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -520,18 +520,18 @@ def get_deadtime(self, _exposure: float | None) -> float: async def prepare(self, trigger_info: TriggerInfo) -> None: """Prepare the detector for acquisition.""" if (exposure := trigger_info.livetime) is not None: - await self._driver.acquire_time.set(exposure) + await self.driver.acquire_time.set(exposure) # Configure trigger mode based on TriggerInfo if trigger_info.trigger == DetectorTrigger.INTERNAL: - await self._driver.trigger_mode.set(EigerTriggerMode.INTERNAL_SERIES) - elif trigger_info.trigger == DetectorTrigger.EXTERNAL: - await self._driver.trigger_mode.set(EigerTriggerMode.EXTERNAL_SERIES) + await self.driver.trigger_mode.set(EigerTriggerMode.INTERNAL_SERIES) + elif trigger_info.trigger == DetectorTrigger.EDGE_TRIGGER: + await self.driver.trigger_mode.set(EigerTriggerMode.EXTERNAL_SERIES) elif trigger_info.trigger in [ DetectorTrigger.VARIABLE_GATE, DetectorTrigger.CONSTANT_GATE, ]: - await self._driver.trigger_mode.set(EigerTriggerMode.EXTERNAL_GATE) + await self.driver.trigger_mode.set(EigerTriggerMode.EXTERNAL_GATE) else: msg = f"Trigger mode {trigger_info.trigger} not supported" raise NotImplementedError(msg) @@ -542,9 +542,9 @@ async def prepare(self, trigger_info: TriggerInfo) -> None: image_mode = ADImageMode.MULTIPLE await asyncio.gather( - self._driver.num_triggers.set(trigger_info.number_of_events), - self._driver.num_images.set(trigger_info.exposures_per_event), - self._driver.image_mode.set(image_mode), + self.driver.num_triggers.set(trigger_info.number_of_events), + self.driver.num_images.set(trigger_info.exposures_per_event), + self.driver.image_mode.set(image_mode), ) diff --git a/tests/test_eiger_async.py b/tests/test_eiger_async.py index 6275a6e4..d5650b8b 100644 --- a/tests/test_eiger_async.py +++ b/tests/test_eiger_async.py @@ -14,8 +14,10 @@ StaticFilenameProvider, StaticPathProvider, init_devices, + TriggerInfo, + DetectorTrigger, ) -from ophyd_async.epics.adcore import ADBaseDatasetDescriber +from ophyd_async.epics.adcore import ADBaseDatasetDescriber, ADImageMode from ophyd_async.testing import ( set_mock_value, ) @@ -25,9 +27,20 @@ EigerDriverIO, EigerHDF5Format, EigerWriter, + EigerDetector, + EigerController, + EigerTriggerMode, ) +@pytest.fixture +def mock_eiger_detector(RE: RunEngine) -> EigerDetector: + path_provider = StaticPathProvider(StaticFilenameProvider("test_eiger")) + with init_devices(mock=True): + detector = EigerDetector("MOCK:EIGER:", path_provider, name="test_eiger") + return detector + + @pytest.fixture def mock_eiger_driver(RE: RunEngine) -> EigerDriverIO: """Create a mock EigerDriverIO for testing.""" @@ -59,6 +72,11 @@ def eiger_writer( return EigerWriter(mock_eiger_driver, mock_path_provider, dataset_describer) +@pytest.fixture +def eiger_controller(mock_eiger_driver: EigerDriverIO) -> EigerController: + return EigerController(mock_eiger_driver) + + @pytest.mark.asyncio async def test_eiger_writer_initialization( eiger_writer: EigerWriter, @@ -295,7 +313,8 @@ async def collect_docs(num_triggers: int): data_docs = [] for i in range(1, num_triggers + 1): async for doc_type, doc in eiger_writer.collect_stream_docs( - name="test_eiger", indices_written=i + _name="", + indices_written=i ): if doc_type == "stream_resource": resource_docs.append(doc) @@ -359,3 +378,50 @@ async def test_eiger_writer_close( # TODO: Test the controller's overridden methods and do an integration test with bluesky plans + tiled readback +@pytest.mark.asyncio +async def test_eiger_controller_prepare(eiger_controller: EigerController) -> None: + trigger_info = TriggerInfo( + number_of_events=1, + livetime=0.01, + deadtime=0.001, + trigger=DetectorTrigger.INTERNAL, + exposure_timeout=1.0, + exposures_per_event=1, + ) + await eiger_controller.prepare(trigger_info) + assert await eiger_controller.driver.acquire_time.get_value() == 0.01 + assert await eiger_controller.driver.trigger_mode.get_value() == EigerTriggerMode.INTERNAL_SERIES + assert await eiger_controller.driver.num_triggers.get_value() == 1 + assert await eiger_controller.driver.num_images.get_value() == 1 + assert await eiger_controller.driver.image_mode.get_value() == ADImageMode.MULTIPLE + + trigger_info = TriggerInfo( + number_of_events=10, + livetime=0.0, + deadtime=0.0, + trigger=DetectorTrigger.EDGE_TRIGGER, + exposure_timeout=10.0, + exposures_per_event=5, + ) + await eiger_controller.prepare(trigger_info) + assert await eiger_controller.driver.acquire_time.get_value() == 0.0 + assert await eiger_controller.driver.trigger_mode.get_value() == EigerTriggerMode.EXTERNAL_SERIES + assert await eiger_controller.driver.num_triggers.get_value() == 10 + assert await eiger_controller.driver.num_images.get_value() == 5 + assert await eiger_controller.driver.image_mode.get_value() == ADImageMode.MULTIPLE + + trigger_info = TriggerInfo( + number_of_events=0, + livetime=None, + deadtime=0.0, + trigger=DetectorTrigger.VARIABLE_GATE, + exposure_timeout=10.0, + exposures_per_event=1, + ) + await eiger_controller.prepare(trigger_info) + assert await eiger_controller.driver.acquire_time.get_value() == 0.0 + assert await eiger_controller.driver.trigger_mode.get_value() == EigerTriggerMode.EXTERNAL_GATE + assert await eiger_controller.driver.num_triggers.get_value() == 0 + assert await eiger_controller.driver.num_images.get_value() == 1 + assert await eiger_controller.driver.image_mode.get_value() == ADImageMode.CONTINUOUS + From f40ae098b8f3290c6310bb91b5fc2b53edaa1c6e Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Wed, 2 Jul 2025 15:19:38 -0400 Subject: [PATCH 11/46] EigerWriter inherits from ADWriter; Workaround for NumTriggers not being available to fileio --- src/cditools/eiger_async.py | 237 +++++++++++++++++------------------ tests/test_eiger_async.py | 240 ++++++++++++++++++++++++------------ 2 files changed, 277 insertions(+), 200 deletions(-) diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index 4ad295fa..184f2cd4 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -5,12 +5,11 @@ from __future__ import annotations import asyncio -from collections.abc import AsyncGenerator, AsyncIterator, Sequence +from collections.abc import AsyncIterator, Sequence from logging import getLogger -from math import ceil from pathlib import Path from typing import Annotated as A -from typing import Any +from typing import Any, cast import numpy as np from bluesky.protocols import StreamAsset @@ -18,7 +17,6 @@ from ophyd_async.core import ( DatasetDescriber, DetectorTrigger, - DetectorWriter, HDFDatasetDescription, HDFDocumentComposer, PathInfo, @@ -27,7 +25,6 @@ SignalRW, StrictEnum, TriggerInfo, - observe_value, ) from ophyd_async.epics.adcore import ( ADBaseController, @@ -35,6 +32,7 @@ ADImageMode, ADWriter, AreaDetector, + NDFileIO, NDPluginBaseIO, ) from ophyd_async.epics.signal import PvSuffix @@ -208,23 +206,6 @@ class EigerDriverIO(ADBaseIO): bit_depth_image: A[SignalR[int], PvSuffix("BitDepthImage_RBV")] count_cutoff: A[SignalR[float], PvSuffix("CountCutoff_RBV")] - # FileWriter Interface - fw_enable: A[SignalRW[bool], PvSuffix.rbv("FWEnable")] - fw_state: A[SignalR[str], PvSuffix("FWState_RBV")] - fw_hdf5_format: A[SignalRW[EigerHDF5Format], PvSuffix.rbv("FWHDF5Format")] - fw_compression: A[SignalRW[bool], PvSuffix.rbv("FWCompression")] - fw_nimgs_per_file: A[SignalRW[float], PvSuffix.rbv("FWNImgsPerFile")] - fw_name_pattern: A[SignalRW[str], PvSuffix.rbv("FWNamePattern")] - sequence_id: A[SignalR[int], PvSuffix("SequenceId")] - save_files: A[SignalRW[bool], PvSuffix.rbv("SaveFiles")] - file_path: A[SignalRW[str], PvSuffix.rbv("FilePath")] - file_owner: A[SignalRW[str], PvSuffix.rbv("FileOwner")] - file_owner_grp: A[SignalRW[str], PvSuffix.rbv("FileOwnerGrp")] - file_perms: A[SignalRW[str], PvSuffix.rbv("FilePerms")] - fw_free: A[SignalR[float], PvSuffix("FWFree_RBV")] - fw_auto_remove: A[SignalRW[bool], PvSuffix.rbv("FWAutoRemove")] - fw_clear: A[SignalRW[float], PvSuffix("FWClear")] - # Stream Interface stream_enable: A[SignalRW[bool], PvSuffix.rbv("StreamEnable")] stream_state: A[SignalR[str], PvSuffix("StreamState_RBV")] @@ -265,60 +246,79 @@ class EigerDriverIO(ADBaseIO): energy_eps: A[SignalRW[float], PvSuffix.rbv("EnergyEps")] -class EigerWriter(DetectorWriter): +class EigerFileIO(NDFileIO): + """FileWriter interface for the Eiger detector.""" + + fw_enable: A[SignalRW[bool], PvSuffix.rbv("FWEnable")] + fw_state: A[SignalR[str], PvSuffix("FWState_RBV")] + fw_hdf5_format: A[SignalRW[EigerHDF5Format], PvSuffix.rbv("FWHDF5Format")] + fw_compression: A[SignalRW[bool], PvSuffix.rbv("FWCompression")] + fw_nimgs_per_file: A[SignalRW[float], PvSuffix.rbv("FWNImgsPerFile")] + fw_name_pattern: A[SignalRW[str], PvSuffix.rbv("FWNamePattern")] + sequence_id: A[SignalR[int], PvSuffix("SequenceId")] + save_files: A[SignalRW[bool], PvSuffix.rbv("SaveFiles")] + file_owner: A[SignalRW[str], PvSuffix.rbv("FileOwner")] + file_owner_grp: A[SignalRW[str], PvSuffix.rbv("FileOwnerGrp")] + file_perms: A[SignalRW[str], PvSuffix.rbv("FilePerms")] + fw_free: A[SignalR[float], PvSuffix("FWFree_RBV")] + fw_auto_remove: A[SignalRW[bool], PvSuffix.rbv("FWAutoRemove")] + fw_clear: A[SignalRW[float], PvSuffix("FWClear")] + + +class EigerWriter(ADWriter[EigerFileIO]): """Eiger-specific file writer using the built-in FileWriter interface.""" + default_suffix: str = "cam1:" + # Forced minimum number of images per file to simplify the logic + _min_num_images_per_file: int = 100000000 + def __init__( self, - driver: EigerDriverIO, + fileio: EigerFileIO, path_provider: PathProvider, dataset_describer: DatasetDescriber, + plugins: dict[str, NDPluginBaseIO] | None = None, ): - self._driver = driver - self._path_provider = path_provider - self._dataset_describer = dataset_describer + super().__init__( + fileio, + path_provider, + dataset_describer, + file_extension=".h5", + mimetype="application/x-hdf5", + plugins=plugins, + ) self._file_info: PathInfo | None = None + self._datasets: list[HDFDatasetDescription] = [] self._current_sequence_id: int | None = None + self._old_fwn_num_images_per_file: int | None = None self._composer: HDFDocumentComposer | None = None - self._exposures_per_event: int = 1 async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataKey]: """Setup file writing for acquisition.""" # Get file path info from path provider self._file_info = self._path_provider() - self._current_sequence_id = await self._driver.sequence_id.get_value() + self._current_sequence_id = await self.fileio.sequence_id.get_value() + + # Force the number of images per file to a large number to simplify the logic + if self._old_fwn_num_images_per_file is None: + self._old_fwn_num_images_per_file = ( + await self.fileio.fw_nimgs_per_file.get_value() + ) + await self.fileio.fw_nimgs_per_file.set(self._min_num_images_per_file) # Set the name pattern with $id replacement similar to original name_pattern = f"{self._file_info.filename}_$id" # Configure the Eiger FileWriter await asyncio.gather( - self._driver.file_path.set(self._file_info.directory_path.as_posix()), - self._driver.fw_name_pattern.set(name_pattern), - self._driver.fw_enable.set(True), - self._driver.save_files.set(True), - self._driver.fw_hdf5_format.set(EigerHDF5Format.LEGACY), - self._driver.data_source.set(EigerDataSource.FILE_WRITER), - ) - - # Get important values from the detector - num_images, num_images_per_file, num_triggers = await asyncio.gather( - self._driver.num_images.get_value(), - self._driver.fw_nimgs_per_file.get_value(), - self._driver.num_triggers.get_value(), + self.fileio.file_path.set(self._file_info.directory_path.as_posix()), + self.fileio.fw_name_pattern.set(name_pattern), + self.fileio.fw_enable.set(True), + self.fileio.save_files.set(True), + self.fileio.fw_hdf5_format.set(EigerHDF5Format.LEGACY), ) - # Exposures per event is a combination of multiple signals, so we can't simply - # set it on the detector, unlike other detector implementations - if num_images != exposures_per_event: - msg = ( - "Mismatch between the number of images set in the detector " - "and the expected number of exposures per event. " - f"Got {num_images} but expected {exposures_per_event}." - ) - raise ValueError(msg) - detector_shape, np_dtype = await asyncio.gather( self._dataset_describer.shape(), self._dataset_describer.np_datatype(), @@ -395,30 +395,40 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK # Cache for use later self._exposures_per_event = exposures_per_event - # Add the array datasets (linked from the master file) - # Linked keys are of the form - # - "/entry/data_000001" - # - "/entry/data_000002" - # - ... - # Example: if exposures_per_event (num_images) is 60, num_triggers is 2, and num_images_per_file is 100, - # then the data_000001 file will have 100 images and the data_000002 filewill have 20 images. - # Put simply, the last file could have less than num_images_per_file images. - total_images = num_triggers * exposures_per_event + # TODO: Add back once we allow setting `num_images_per_file` again + # # Add the array datasets (linked from the master file) + # # Linked keys are of the form + # # - "/entry/data_000001" + # # - "/entry/data_000002" + # # - ... + # # Example: if exposures_per_event (num_images) is 60, num_triggers is 2, and num_images_per_file is 100, + # # then the data_000001 file will have 100 images and the data_000002 filewill have 20 images. + # # Put simply, the last file could have less than num_images_per_file images. + # total_images = num_triggers * exposures_per_event + # frame_datasets = [ + # HDFDatasetDescription( + # data_key=f"{name}_{i}", + # dataset=f"/entry/data_{i:06d}", + # shape=( + # min( + # num_images_per_file, + # total_images - (i - 1) * num_images_per_file, + # ), + # *detector_shape, + # ), + # dtype_numpy=np_dtype, + # chunk_shape=(1, *detector_shape), + # ) + # for i in range(1, ceil(total_images / num_images_per_file) + 1) + # ] frame_datasets = [ HDFDatasetDescription( - data_key=f"{name}_{i}", - dataset=f"/entry/data_{i:06d}", - shape=( - min( - num_images_per_file, - total_images - (i - 1) * num_images_per_file, - ), - *detector_shape, - ), + data_key=f"{name}_1", + dataset=f"/entry/data_{1:06d}", + shape=(exposures_per_event, *detector_shape), dtype_numpy=np_dtype, chunk_shape=(1, *detector_shape), ) - for i in range(1, ceil(total_images / num_images_per_file) + 1) ] self._datasets = master_datasets + frame_datasets @@ -437,41 +447,14 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK } @property - def _master_file_path(self) -> Path: + def _master_file_path(self) -> Path | None: if self._current_sequence_id is None or self._file_info is None: - msg = "Must call EigerWriter.open() before accessing master file path" - raise ValueError(msg) + return None return ( self._file_info.directory_path / f"{self._file_info.filename}_{self._current_sequence_id}_master.h5" ) - async def compute_index(self, num_images_counter: int) -> int: - if await self._driver.num_images.get_value() != self._exposures_per_event: - msg = "Detected change to the number of images during acquisition. This is not allowed." - raise RuntimeError(msg) - return num_images_counter // self._exposures_per_event - - async def observe_indices_written( - self, timeout: float = 10.0 - ) -> AsyncGenerator[int, None]: - """Monitor the number of files written by the Eiger FileWriter.""" - async for num_images_counter in observe_value( - self._driver.num_images_counter, timeout - ): - yield await self.compute_index(num_images_counter) - - async def get_indices_written(self) -> int: - """Get the current number of indices written. - - Since Eiger defines the number of triggers, we want each trigger - to correspond to a single index. This is in contrast to the ophyd-sync - implementation, where the `num_triggers` was ignored. - """ - return await self.compute_index( - await self._driver.num_images_counter.get_value() - ) - async def collect_stream_docs( self, _name: str, indices_written: int ) -> AsyncIterator[StreamAsset]: @@ -491,18 +474,20 @@ async def collect_stream_docs( async def close(self) -> None: """Clean up file writing after acquisition and validate files exist.""" - # Disable file writer + # Disable file writer and reset number of images per file await asyncio.gather( - self._driver.fw_enable.set(False), - self._driver.save_files.set(False), + self.fileio.fw_enable.set(False), + self.fileio.save_files.set(False), + self.fileio.fw_nimgs_per_file.set(self._old_fwn_num_images_per_file), ) - if not self._master_file_path.exists(): + if self._master_file_path is not None and not self._master_file_path.exists(): logger.warning("Master file was not written: %s", self._master_file_path) self._composer = None self._file_info = None self._current_sequence_id = None + self._old_fwn_num_images_per_file = None class EigerController(ADBaseController[EigerDriverIO]): @@ -549,14 +534,14 @@ async def prepare(self, trigger_info: TriggerInfo) -> None: class EigerDetector(AreaDetector[EigerController]): - """Eiger detector implementation using AreaDetector pattern.""" + """Eiger detector implementation using the AreaDetector pattern.""" def __init__( self, prefix: str, path_provider: PathProvider, driver_suffix: str = "cam1:", - writer_cls: type[DetectorWriter] = EigerWriter, + writer_cls: type[ADWriter] = EigerWriter, fileio_suffix: str | None = None, name: str = "", config_sigs: Sequence[SignalR] = (), @@ -564,24 +549,13 @@ def __init__( ): driver = EigerDriverIO(prefix + driver_suffix) controller = EigerController(driver) - - # If the writer class is an ADWriter, use the with_io method - # since we want to use one of the AD plugins. - # Otherwise, use the internal file writer. - if issubclass(writer_cls, ADWriter): - writer = writer_cls.with_io( - prefix, - path_provider, - dataset_source=driver, - fileio_suffix=fileio_suffix, - plugins=plugins, - ) - else: - if fileio_suffix is not None or plugins is not None: - logger.warning( - "Ignoring params fileio_suffix and plugins for non-ADWriter writer" - ) - writer = writer_cls(driver, path_provider) + writer = writer_cls.with_io( + prefix, + path_provider, + dataset_source=driver, + fileio_suffix=fileio_suffix, + plugins=plugins, + ) super().__init__( controller=controller, @@ -590,3 +564,18 @@ def __init__( name=name, config_sigs=config_sigs, ) + + # TODO: Fix typing in base class + self.driver: EigerDriverIO = cast(EigerDriverIO, self.driver) + + async def stage(self) -> None: + """Prepare the eiger detector for acquisition. + + If the writer is an EigerWriter, set the data source to FILE_WRITER. + Otherwise, set the data source to STREAM. + """ + await super().stage() + if isinstance(self.fileio, EigerFileIO): + await self.driver.data_source.set(EigerDataSource.FILE_WRITER) + else: + await self.driver.data_source.set(EigerDataSource.STREAM) diff --git a/tests/test_eiger_async.py b/tests/test_eiger_async.py index d5650b8b..6cd97f17 100644 --- a/tests/test_eiger_async.py +++ b/tests/test_eiger_async.py @@ -10,12 +10,12 @@ import pytest from bluesky.run_engine import RunEngine from ophyd_async.core import ( + DetectorTrigger, PathProvider, StaticFilenameProvider, StaticPathProvider, - init_devices, TriggerInfo, - DetectorTrigger, + init_devices, ) from ophyd_async.epics.adcore import ADBaseDatasetDescriber, ADImageMode from ophyd_async.testing import ( @@ -23,21 +23,27 @@ ) from cditools.eiger_async import ( + EigerController, EigerDataSource, + EigerDetector, EigerDriverIO, + EigerFileIO, EigerHDF5Format, - EigerWriter, - EigerDetector, - EigerController, EigerTriggerMode, + EigerWriter, ) @pytest.fixture def mock_eiger_detector(RE: RunEngine) -> EigerDetector: - path_provider = StaticPathProvider(StaticFilenameProvider("test_eiger")) + path_provider = StaticPathProvider( + StaticFilenameProvider("test_eiger"), directory_path="/tmp/test_data" + ) with init_devices(mock=True): detector = EigerDetector("MOCK:EIGER:", path_provider, name="test_eiger") + set_mock_value(detector.driver.array_size_x, 2048) + set_mock_value(detector.driver.array_size_y, 2048) + set_mock_value(detector.driver.data_type, "UInt16") return detector @@ -45,7 +51,7 @@ def mock_eiger_detector(RE: RunEngine) -> EigerDetector: def mock_eiger_driver(RE: RunEngine) -> EigerDriverIO: """Create a mock EigerDriverIO for testing.""" with init_devices(mock=True): - driver = EigerDriverIO("MOCK:EIGER:CAM:") + driver = EigerDriverIO("MOCK:EIGER:cam1:") # Set up some default mock values set_mock_value(driver.array_size_x, 2048) @@ -55,6 +61,14 @@ def mock_eiger_driver(RE: RunEngine) -> EigerDriverIO: return driver +@pytest.fixture +def mock_eiger_fileio(RE: RunEngine) -> EigerFileIO: + with init_devices(mock=True): + fileio = EigerFileIO("MOCK:EIGER:cam1:") + + return fileio + + @pytest.fixture def mock_path_provider() -> PathProvider: """Create a mock path provider for testing.""" @@ -65,11 +79,13 @@ def mock_path_provider() -> PathProvider: @pytest.fixture def eiger_writer( - mock_eiger_driver: EigerDriverIO, mock_path_provider: PathProvider + mock_eiger_driver: EigerDriverIO, + mock_eiger_fileio: EigerFileIO, + mock_path_provider: PathProvider, ) -> EigerWriter: """Create an EigerWriter instance for testing.""" dataset_describer = ADBaseDatasetDescriber(mock_eiger_driver) - return EigerWriter(mock_eiger_driver, mock_path_provider, dataset_describer) + return EigerWriter(mock_eiger_fileio, mock_path_provider, dataset_describer) @pytest.fixture @@ -80,11 +96,11 @@ def eiger_controller(mock_eiger_driver: EigerDriverIO) -> EigerController: @pytest.mark.asyncio async def test_eiger_writer_initialization( eiger_writer: EigerWriter, - mock_eiger_driver: EigerDriverIO, + mock_eiger_fileio: EigerFileIO, mock_path_provider: PathProvider, ): """Test that EigerWriter initializes correctly.""" - assert eiger_writer._driver is mock_eiger_driver + assert eiger_writer.fileio is mock_eiger_fileio assert eiger_writer._path_provider is mock_path_provider assert eiger_writer._dataset_describer is not None assert eiger_writer._file_info is None @@ -94,7 +110,9 @@ async def test_eiger_writer_initialization( @pytest.mark.asyncio async def test_eiger_writer_open( - eiger_writer: EigerWriter, mock_eiger_driver: EigerDriverIO + eiger_writer: EigerWriter, + mock_eiger_driver: EigerDriverIO, + mock_eiger_fileio: EigerFileIO, ) -> None: """Test the open method configures the detector correctly.""" array_size_x, array_size_y, data_type = await asyncio.gather( @@ -104,18 +122,15 @@ async def test_eiger_writer_open( ) # Case 1: 1 image per file, 1 image, 1 trigger - set_mock_value(mock_eiger_driver.sequence_id, 1) - set_mock_value(mock_eiger_driver.fw_nimgs_per_file, 1) + set_mock_value(mock_eiger_fileio.sequence_id, 1) + set_mock_value(mock_eiger_fileio.fw_nimgs_per_file, 1) set_mock_value(mock_eiger_driver.num_images, 1) set_mock_value(mock_eiger_driver.num_triggers, 1) description = await eiger_writer.open(name="test_eiger", exposures_per_event=1) - assert await mock_eiger_driver.fw_enable.get_value() is True - assert await mock_eiger_driver.save_files.get_value() is True - assert await mock_eiger_driver.fw_hdf5_format.get_value() == EigerHDF5Format.LEGACY - assert ( - await mock_eiger_driver.data_source.get_value() == EigerDataSource.FILE_WRITER - ) + assert await mock_eiger_fileio.fw_enable.get_value() is True + assert await mock_eiger_fileio.save_files.get_value() is True + assert await mock_eiger_fileio.fw_hdf5_format.get_value() == EigerHDF5Format.LEGACY assert description.keys() == { "test_eiger_y_pixel_size", "test_eiger_x_pixel_size", @@ -134,8 +149,8 @@ async def test_eiger_writer_open( # Case 2: 4 images per file, 11 images, 2 triggers # Expect 6 files, the first 5 will have 4 images, the last will have 2 - set_mock_value(mock_eiger_driver.sequence_id, 2) - set_mock_value(mock_eiger_driver.fw_nimgs_per_file, 4) + set_mock_value(mock_eiger_fileio.sequence_id, 2) + set_mock_value(mock_eiger_fileio.fw_nimgs_per_file, 4) set_mock_value(mock_eiger_driver.num_images, 11) set_mock_value(mock_eiger_driver.num_triggers, 2) description = await eiger_writer.open( @@ -153,101 +168,116 @@ async def test_eiger_writer_open( "test_eiger_count_time", "test_eiger_pixel_mask", "test_eiger_1", - "test_eiger_2", - "test_eiger_3", - "test_eiger_4", - "test_eiger_5", - "test_eiger_6", } - - for i in range(1, 6): - data_key = description[f"test_eiger_{i}"] - assert tuple(data_key["shape"]) == (4, array_size_x, array_size_y) - assert data_key["dtype"] == "array" - assert data_key["dtype_numpy"] == np.dtype(data_type.lower()).str - assert data_key["external"] == "STREAM:" - assert data_key["source"] == "/tmp/test_data/test_eiger_2_master.h5" - data_key = description["test_eiger_6"] - assert tuple(data_key["shape"]) == (2, array_size_x, array_size_y) + data_key = description["test_eiger_1"] + assert tuple(data_key["shape"]) == (11, array_size_x, array_size_y) assert data_key["dtype"] == "array" assert data_key["dtype_numpy"] == np.dtype(data_type.lower()).str assert data_key["external"] == "STREAM:" assert data_key["source"] == "/tmp/test_data/test_eiger_2_master.h5" + # # TODO: Add back when nimages_per_file is no longer hardcoded + # "test_eiger_2", + # "test_eiger_3", + # "test_eiger_4", + # "test_eiger_5", + # "test_eiger_6", + # } + + # for i in range(1, 6): + # data_key = description[f"test_eiger_{i}"] + # assert tuple(data_key["shape"]) == (4, array_size_x, array_size_y) + # assert data_key["dtype"] == "array" + # assert data_key["dtype_numpy"] == np.dtype(data_type.lower()).str + # assert data_key["external"] == "STREAM:" + # assert data_key["source"] == "/tmp/test_data/test_eiger_2_master.h5" + # data_key = description["test_eiger_6"] + # assert tuple(data_key["shape"]) == (2, array_size_x, array_size_y) + # assert data_key["dtype"] == "array" + # assert data_key["dtype_numpy"] == np.dtype(data_type.lower()).str + # assert data_key["external"] == "STREAM:" + # assert data_key["source"] == "/tmp/test_data/test_eiger_2_master.h5" + @pytest.mark.asyncio -async def test_eiger_writer_get_indices_written(eiger_writer, mock_eiger_driver): +async def test_eiger_writer_get_indices_written( + eiger_writer: EigerWriter, + mock_eiger_driver: EigerDriverIO, + mock_eiger_fileio: EigerFileIO, +): """Test getting the number of indices written.""" - set_mock_value(mock_eiger_driver.sequence_id, 1) - set_mock_value(mock_eiger_driver.fw_nimgs_per_file, 1) + set_mock_value(mock_eiger_fileio.sequence_id, 1) + set_mock_value(mock_eiger_fileio.fw_nimgs_per_file, 1) # Case 1: 1 image, 1 trigger set_mock_value(mock_eiger_driver.num_triggers, 1) set_mock_value(mock_eiger_driver.num_images, 1) - set_mock_value(mock_eiger_driver.num_images_counter, 0) + set_mock_value(mock_eiger_fileio.num_captured, 0) await eiger_writer.open( name="test_eiger", exposures_per_event=await mock_eiger_driver.num_images.get_value(), ) assert await eiger_writer.get_indices_written() == 0 - set_mock_value(mock_eiger_driver.num_images_counter, 1) + set_mock_value(mock_eiger_fileio.num_captured, 1) assert await eiger_writer.get_indices_written() == 1 # Case 2: 1 image, 5 triggers set_mock_value(mock_eiger_driver.num_triggers, 5) set_mock_value(mock_eiger_driver.num_images, 1) - set_mock_value(mock_eiger_driver.num_images_counter, 0) + set_mock_value(mock_eiger_fileio.num_captured, 0) await eiger_writer.open( name="test_eiger", exposures_per_event=await mock_eiger_driver.num_images.get_value(), ) assert await eiger_writer.get_indices_written() == 0 - set_mock_value(mock_eiger_driver.num_images_counter, 1) + set_mock_value(mock_eiger_fileio.num_captured, 1) assert await eiger_writer.get_indices_written() == 1 - set_mock_value(mock_eiger_driver.num_images_counter, 3) + set_mock_value(mock_eiger_fileio.num_captured, 3) assert await eiger_writer.get_indices_written() == 3 - set_mock_value(mock_eiger_driver.num_images_counter, 5) + set_mock_value(mock_eiger_fileio.num_captured, 5) assert await eiger_writer.get_indices_written() == 5 # Case 3: 5 images, 2 triggers set_mock_value(mock_eiger_driver.num_triggers, 2) set_mock_value(mock_eiger_driver.num_images, 5) - set_mock_value(mock_eiger_driver.num_images_counter, 0) + set_mock_value(mock_eiger_fileio.num_captured, 0) await eiger_writer.open( name="test_eiger", exposures_per_event=await mock_eiger_driver.num_images.get_value(), ) assert await eiger_writer.get_indices_written() == 0 - set_mock_value(mock_eiger_driver.num_images_counter, 4) + set_mock_value(mock_eiger_fileio.num_captured, 4) assert await eiger_writer.get_indices_written() == 0 - set_mock_value(mock_eiger_driver.num_images_counter, 5) + set_mock_value(mock_eiger_fileio.num_captured, 5) assert await eiger_writer.get_indices_written() == 1 - set_mock_value(mock_eiger_driver.num_images_counter, 9) + set_mock_value(mock_eiger_fileio.num_captured, 9) assert await eiger_writer.get_indices_written() == 1 - set_mock_value(mock_eiger_driver.num_images_counter, 10) + set_mock_value(mock_eiger_fileio.num_captured, 10) assert await eiger_writer.get_indices_written() == 2 @pytest.mark.asyncio async def test_eiger_writer_observe_indices_written( - eiger_writer: EigerWriter, mock_eiger_driver: EigerDriverIO + eiger_writer: EigerWriter, + mock_eiger_driver: EigerDriverIO, + mock_eiger_fileio: EigerFileIO, ) -> None: """Test observing indices as they are written.""" - set_mock_value(mock_eiger_driver.sequence_id, 1) - set_mock_value(mock_eiger_driver.fw_nimgs_per_file, 1) + set_mock_value(mock_eiger_fileio.sequence_id, 1) + set_mock_value(mock_eiger_fileio.fw_nimgs_per_file, 1) async def _simulate_writing_indices( num_images: int, num_triggers: int, acquire_time: float = 0.01 ) -> list[int]: # Create an async generator to track yielded indices observed_indices = [] - set_mock_value(mock_eiger_driver.num_images_counter, 0) + set_mock_value(mock_eiger_fileio.num_captured, 0) async def _simulate_acquisition(): """Simulate the detector writing images by incrementing sequence_id.""" for i in range(1, num_images * num_triggers + 1): await asyncio.sleep(acquire_time) - set_mock_value(mock_eiger_driver.num_images_counter, i) + set_mock_value(mock_eiger_fileio.num_captured, i) async def _complete(): """Helper function to collect observed indices.""" @@ -304,7 +334,9 @@ async def _complete(): @pytest.mark.asyncio async def test_eiger_writer_collect_stream_docs( - eiger_writer: EigerWriter, mock_eiger_driver: EigerDriverIO + eiger_writer: EigerWriter, + mock_eiger_driver: EigerDriverIO, + mock_eiger_fileio: EigerFileIO, ) -> None: """Test collecting stream documents.""" @@ -313,8 +345,7 @@ async def collect_docs(num_triggers: int): data_docs = [] for i in range(1, num_triggers + 1): async for doc_type, doc in eiger_writer.collect_stream_docs( - _name="", - indices_written=i + _name="", indices_written=i ): if doc_type == "stream_resource": resource_docs.append(doc) @@ -322,8 +353,8 @@ async def collect_docs(num_triggers: int): data_docs.append(doc) return resource_docs, data_docs - set_mock_value(mock_eiger_driver.sequence_id, 1) - set_mock_value(mock_eiger_driver.fw_nimgs_per_file, 1) + set_mock_value(mock_eiger_fileio.sequence_id, 1) + set_mock_value(mock_eiger_fileio.fw_nimgs_per_file, 1) set_mock_value(mock_eiger_driver.num_images, 1) set_mock_value(mock_eiger_driver.num_triggers, 1) await eiger_writer.open(name="test_eiger", exposures_per_event=1) @@ -340,13 +371,13 @@ async def collect_docs(num_triggers: int): await eiger_writer.close() set_mock_value(mock_eiger_driver.num_triggers, 3) - set_mock_value(mock_eiger_driver.sequence_id, 2) + set_mock_value(mock_eiger_fileio.sequence_id, 2) await eiger_writer.open(name="test_eiger", exposures_per_event=1) resource_docs, data_docs = await collect_docs( num_triggers=await mock_eiger_driver.num_triggers.get_value() ) - assert len(resource_docs) == 12 - assert len(data_docs) == 36 + assert len(resource_docs) == 10 + assert len(data_docs) == 30 assert ( resource_docs[0]["uri"] == "file://localhost/tmp/test_data/test_eiger_2_master.h5" @@ -355,26 +386,29 @@ async def collect_docs(num_triggers: int): @pytest.mark.asyncio async def test_eiger_writer_close( - eiger_writer: EigerWriter, mock_eiger_driver: EigerDriverIO + eiger_writer: EigerWriter, + mock_eiger_driver: EigerDriverIO, + mock_eiger_fileio: EigerFileIO, ) -> None: """Test closing the writer.""" # Verify the writing was enabled - set_mock_value(mock_eiger_driver.sequence_id, 1) + set_mock_value(mock_eiger_fileio.sequence_id, 1) set_mock_value(mock_eiger_driver.num_images, 1) set_mock_value(mock_eiger_driver.num_triggers, 1) - set_mock_value(mock_eiger_driver.fw_nimgs_per_file, 1) + set_mock_value(mock_eiger_fileio.fw_nimgs_per_file, 1) await eiger_writer.open(name="test_eiger", exposures_per_event=1) - assert await mock_eiger_driver.fw_enable.get_value() is True - assert await mock_eiger_driver.save_files.get_value() is True + assert await mock_eiger_fileio.fw_enable.get_value() is True + assert await mock_eiger_fileio.save_files.get_value() is True # Verify the writing was disabled await eiger_writer.close() - assert await mock_eiger_driver.fw_enable.get_value() is False - assert await mock_eiger_driver.save_files.get_value() is False + assert await mock_eiger_fileio.fw_enable.get_value() is False + assert await mock_eiger_fileio.save_files.get_value() is False assert eiger_writer._composer is None assert eiger_writer._current_sequence_id is None assert eiger_writer._file_info is None + assert await mock_eiger_fileio.fw_nimgs_per_file.get_value() == 1 # TODO: Test the controller's overridden methods and do an integration test with bluesky plans + tiled readback @@ -390,7 +424,10 @@ async def test_eiger_controller_prepare(eiger_controller: EigerController) -> No ) await eiger_controller.prepare(trigger_info) assert await eiger_controller.driver.acquire_time.get_value() == 0.01 - assert await eiger_controller.driver.trigger_mode.get_value() == EigerTriggerMode.INTERNAL_SERIES + assert ( + await eiger_controller.driver.trigger_mode.get_value() + == EigerTriggerMode.INTERNAL_SERIES + ) assert await eiger_controller.driver.num_triggers.get_value() == 1 assert await eiger_controller.driver.num_images.get_value() == 1 assert await eiger_controller.driver.image_mode.get_value() == ADImageMode.MULTIPLE @@ -405,11 +442,14 @@ async def test_eiger_controller_prepare(eiger_controller: EigerController) -> No ) await eiger_controller.prepare(trigger_info) assert await eiger_controller.driver.acquire_time.get_value() == 0.0 - assert await eiger_controller.driver.trigger_mode.get_value() == EigerTriggerMode.EXTERNAL_SERIES + assert ( + await eiger_controller.driver.trigger_mode.get_value() + == EigerTriggerMode.EXTERNAL_SERIES + ) assert await eiger_controller.driver.num_triggers.get_value() == 10 assert await eiger_controller.driver.num_images.get_value() == 5 assert await eiger_controller.driver.image_mode.get_value() == ADImageMode.MULTIPLE - + trigger_info = TriggerInfo( number_of_events=0, livetime=None, @@ -420,8 +460,56 @@ async def test_eiger_controller_prepare(eiger_controller: EigerController) -> No ) await eiger_controller.prepare(trigger_info) assert await eiger_controller.driver.acquire_time.get_value() == 0.0 - assert await eiger_controller.driver.trigger_mode.get_value() == EigerTriggerMode.EXTERNAL_GATE + assert ( + await eiger_controller.driver.trigger_mode.get_value() + == EigerTriggerMode.EXTERNAL_GATE + ) assert await eiger_controller.driver.num_triggers.get_value() == 0 assert await eiger_controller.driver.num_images.get_value() == 1 - assert await eiger_controller.driver.image_mode.get_value() == ADImageMode.CONTINUOUS + assert ( + await eiger_controller.driver.image_mode.get_value() == ADImageMode.CONTINUOUS + ) + +@pytest.mark.asyncio +async def test_eiger_detector(mock_eiger_detector: EigerDetector) -> None: + set_mock_value(mock_eiger_detector.driver.num_images, 1) + set_mock_value(mock_eiger_detector.driver.num_triggers, 2) + set_mock_value(mock_eiger_detector.driver.acquire_period, 0.01) + + async def _simulate_one_trigger(num_captured: int): + await asyncio.sleep(await mock_eiger_detector.driver.acquire_period.get_value()) + set_mock_value(mock_eiger_detector.fileio.num_captured, num_captured) + + # Standalone methods + await mock_eiger_detector.describe() + + # Case 1 - Step Scan: stage, trigger, read, trigger, read, unstage + await mock_eiger_detector.stage() + assert ( + await mock_eiger_detector.driver.data_source.get_value() + == EigerDataSource.FILE_WRITER + ) + status = mock_eiger_detector.trigger() + await _simulate_one_trigger(1) + await status + await mock_eiger_detector.read() + status = mock_eiger_detector.trigger() + await _simulate_one_trigger(2) + await status + await mock_eiger_detector.read() + await mock_eiger_detector.unstage() + + # Case 2 - Fly Scan: prepare, kickoff, complete + await mock_eiger_detector.prepare( + TriggerInfo( + number_of_events=1, + livetime=0.01, + deadtime=0.001, + trigger=DetectorTrigger.INTERNAL, + exposure_timeout=1.0, + exposures_per_event=1, + ) + ) + await mock_eiger_detector.kickoff() + await mock_eiger_detector.complete() From d6e4bc7f79990dd59f677c427cc4f9f324ee3fb6 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Wed, 2 Jul 2025 15:26:45 -0400 Subject: [PATCH 12/46] Better acknowledgement of force minimum nimgs_per_file --- src/cditools/eiger_async.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index 184f2cd4..d83991c4 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -269,8 +269,8 @@ class EigerWriter(ADWriter[EigerFileIO]): """Eiger-specific file writer using the built-in FileWriter interface.""" default_suffix: str = "cam1:" - # Forced minimum number of images per file to simplify the logic - _min_num_images_per_file: int = 100000000 + # Forced minimum number of images per file to force a single HDF5 file + _min_num_images_per_file: int = 1e8 def __init__( self, @@ -306,6 +306,10 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK await self.fileio.fw_nimgs_per_file.get_value() ) await self.fileio.fw_nimgs_per_file.set(self._min_num_images_per_file) + logger.warning( + "Setting fw_nimgs_per_file to %d to force writing to a single HDF5 file", + self._min_num_images_per_file, + ) # Set the name pattern with $id replacement similar to original name_pattern = f"{self._file_info.filename}_$id" From 342138c57e4cf11f7309963e5fb65f069d71d054 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Wed, 2 Jul 2025 16:57:58 -0400 Subject: [PATCH 13/46] Progress on integration tests with tiled and RE --- pixi.lock | 1394 ++++++++++++++++++++++++++++++++++- pyproject.toml | 7 +- src/cditools/eiger_async.py | 13 +- tests/conftest.py | 17 + tests/test_eiger_async.py | 127 +++- 5 files changed, 1548 insertions(+), 10 deletions(-) diff --git a/pixi.lock b/pixi.lock index 692de071..15f2fb0f 100644 --- a/pixi.lock +++ b/pixi.lock @@ -101,90 +101,176 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - pypi: https://files.pythonhosted.org/packages/d3/49/b5e260deff3d218a17fe23a1313bb3c033d846bf74505c297f74d2c8abfe/adbc_driver_manager-1.6.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c1/45/2d3514fd2bf7ee7e1e28c45fdb70f2a27db2f8cc8af7e7c28f846dd7b99d/adbc_driver_postgresql-1.6.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/92/ec/ce41f3c84b5b4f762e4de9072574714c623e959924bf1bbaf1548612cb6e/adbc_driver_sqlite-1.6.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/5b/08/185c3b29b0698328b202e6c965c23187e2e29ead78cb468aab0a09ee97fc/aioca-1.8.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dd/e2/88e425adac5ad887a087c38d04fe2030010572a3e0e627f8a6e8c33eeda8/alembic-1.16.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/6a/885bc91484e1aa8f618f6f0228d76d0e67000b0fdd6090673b777e311913/asciitree-0.3.3.tar.gz + - pypi: https://files.pythonhosted.org/packages/d9/ab/6936e2663c47a926e0659437b9333ad87d1ff49b1375d239026e0a268eba/asgi_correlation_id-4.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/19/139227a6e67f407b9c386cb594d9628c6c78c9024f26df87c912fabd4368/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d2/af/92435ec3cfe35bd738a3ba2332c8cbdb50f0cc66d1f9943a1a457990fcd6/awkward-2.8.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2b/cf/9424352bd9568bd56eb88e5fc8400a6c5bd3c827fdeeae202fcaaf3c1910/awkward_cpp-46-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c9/38/8815054e054e45fda3c9d1e25ac77e6387d10d8e33c64bcf79f432b3c4fa/blosc2-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/fc/30/f7da949893bf0d8525acbaee256cf55430332852225f17e18a7f8783dd6f/bluesky-1.14.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/00/f0/2ef431fe4141f5e334759d73e81120492b23b2824336883a91ac04ba710b/cachetools-6.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/54/346f681c24a9c3a08e2e74dcee2555ccd1081705b46f791f7b228e177d06/canonicaljson-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e9/ad/0ce6cef4823d6c44b87e9b26b2cb284f6e25143ea0ab8215ac836bda825e/caproto-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/e8/64c37fadfc2816a7701fa8a6ed8d87327c7d54eacfbfb6edab14a2f2be75/cloudpickle-3.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-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/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ec/ef/3333295ed58d900a13c92806b67e62f27876845a9a908c939f040887cca9/cryptography-45.0.5-cp311-abi3-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/30/53b0844a7a4c6b041b111b24ca15cc9b8661a86fe1f6aaeb2d0d7f0fb1f2/dask-2025.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/11/79/479e2194c9096b92aecdf33634ae948d2be306c6011673e98ee1917f32c2/dpkt-1.9.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/90/a6/c8577b741974f106e24f8eb3efedc399be1a23cbbdcf49dd4bea5bb8aa4e/duckdb-1.3.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/cb/a3/460c57f094a4a165c84a1341c373b0a4f5ec6ac244b998d5021aade89b77/ecdsa-0.19.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/35/a8/365059bbcd4572cbc41de17fd5b682be5868b218c3c5479071865cab9078/entrypoints-0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4a/18/3853de30cc92c164eccff6e62599ef88d0aa3b64740973b63e13f88145de/epicscorelibs-7.0.7.99.1.1.tar.gz + - pypi: https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/f2/47e3f0e06c3bc3834db2a4ed01641e1ce493108461b9540dd5d2fb60d334/event_model-1.22.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/53/50/b1222562c6d270fea83e9c9075b8e8600b8479150a18e4516a6138b980d1/fastapi-0.115.14-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/bf/fd60001b3abc5222d8eaa4a204cd8c0ae78e75adc688f33ce4bf25b7fafa/fasteners-0.19-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.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/fe/5e/3be305568fe5f34448807976dc82fc151d76c3e0e03958f34770286278c1/flexparser-0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bb/61/78c7b3851add1481b048b5fdc29067397a1784e2910592bc81bb3f608635/fsspec-2025.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/26/f2/ad51331a157c7015c675702e2d5230c243695c788f8f75feba1af32b3617/greenlet-3.2.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4e/27/ca28b77f4cf613282e4ea9bac360cdfa8db8b4d2154e2f0bd82fd26baeb3/h5netcdf-1.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fd/41/bc2df86b72965775f6d621e0ee269a5f3ac23e8f870abf519de9c7d93b4d/h5py-3.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ba/db/8d85bd3f5b9ee894518f9d9ed1019fd8b7014340beca512dfb76d5632ec9/hdf5plugin-5.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_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/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2b/d3/85feeba1d097b81a44bcffa6a0beab7b4dfffe78e82fc54978d3ac380736/identify-2.6.10-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/9d/0fb148dc4d6fa4a7dd1d8378168d9b4cd8d4560a6fbf6f0121c5fc34eb68/importlib_metadata-8.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/9d/4ff2adf55d1b6e3777b0303fdbe5b723f76e46cba4a53a32fe82260d2077/ipython-9.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/c1/fb2c66d92b5d0167c57042b784456ee3f8531a997726c88cf6f012a22da6/json_merge_patch-0.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d2/1b/656f5a357de7135a3777bd735cc7c9b8f23b4d37465505bd0eaf4be9befe/llvmlite-0.44.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/97/12/a1f2f4fdc6b7159c0d12249456f9fe454665b6126e98dbee9f2bd3cf735c/lz4-4.4.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2d/7b/2c1d74ca6c94f70a1add74a8393a0138172207dc5de6fc6269483519d048/msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.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/b5/07/c64b0c8416f604f6990da5d1fa97c9de1278a4eec1efcc63b71053b4f0c0/ndindex-1.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a6/91/86a6eac449ddfae239e93ffc1918cf33fd9bab35c04d1e963b311e347a73/netifaces-0.11.0.tar.gz - pypi: https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/e0/5ea04e7ad2c39288c0f0f9e8d47638ad70f28e275d092733b5817cf243c9/numba-0.61.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fa/91/d96999b41e3146b6c0ce6bddc5ad85803cb4d743c95394562c2a4bb8cded/numcodecs-0.15.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b9/7d/4911f40d3610fc5557029f0d1f20ef9f571488319567ac4d8ee6d0978ee6/numexpr-2.11.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/aa/fc/ebfd32c3e124e6a1043e19c0ab0769818aa69050ce5589b63d05ff185526/numpy-2.2.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.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/12/f2/89ea3361a305466bc6460a532188830351220b5f0851a5fa133155c16eca/opentelemetry_api-1.32.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/5e/f5ded172af40b7e7ae60697a2b2096cec53e45344edce41e91a74425473c/ophyd-1.10.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/85/38/6b455b1d6f0b9aa3db4e8888b797f77d856a8350d836c66a43d1078573b2/ophyd_async-0.10.0a4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6d/4c/2bda09855c6b5f2c055034c9eda1529967b042ff8d81a05005115c4e6772/orjson-3.10.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b7/16/bd2f5904557265882108dc2e04f18abc05ab0c2b7082ae9430091daf1d5c/Pint-0.24.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/ae/ec06af4fe3ee72d16973474f122541746196aaa16cea6f66d18b963c6177/prometheus_client-0.22.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/be/27/43a47fa0ff9053ab5203bb3faeec435d43c0d8bfa40179bfd076cdbd4e1c/pyarrow-20.0.0-cp313-cp313-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.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/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/30/05/ce271016e351fddc8399e546f6e23761967ee09c8c568bbfbecb0c150171/pytest_asyncio-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.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/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/05/7c/8abb70f9017a231c6c961a8941403ed6557664c0913e1bf413cbdc039e75/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_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/04/9e/91fe10ea09e2383f0d0531cf3da765fa01b3b9918e85df92081813259cf4/ruamel.yaml-0.18.12-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1f/8f/ecfbe2123ade605c49ef769788f79c38ddb1c8fa81e01f4dbf5cf1a44b16/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/0e/2c/1e364cc92970075d7d04c69c928430b23e43a433f044474f57e425cbed37/ruff-0.11.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b9/36/c101788fad13e8ea65c5b3d3dee8ff996500800cd554ae6ff72143690247/setuptools_dso-2.12.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8d/fd/42a1720542199ae6ff0f9c37bbd55dd3033ddd7bbe00d68cde09d6824887/sparse-0.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5e/51/5ba9ea3246ea068630acf35a6ba0d181e99f1af1afd17e159eac7e8bc2b8/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/ba/d03f7ee711391af1d5f4dd7c44f8abdd06bce247028af2441ba8f6ff329b/stamina-25.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/d8/1ba8f32bfc9cb69e37edeca93738e883f478fbe84ae401f72c0d8d507841/tifffile-2025.6.11-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/37/63/383de8d9400a52d55683940387b160efb4f6b35bfb7645a443008780e2a9/tiled-0.1.0b29-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/03/98/eb27cc78ad3af8e302c9d8ff4977f5026676e130d28dd7578132a457170c/toolz-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/4c/ed/3cfeb48175f0671ec430ede81f628f9fb2b1084c9064ca67ebe8c0ed6a05/virtualenv-20.30.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/25/0d/4d564798a49bf5482a4fa9416dea6b6c0733a3b5700cb8a5a503c4b15853/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/82/8a/6b50c1dd2260d407c1a499d47cf829f59f07007e0dcebafdabb24d1d26a5/xarray-2025.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5e/d8/9ffd8c237b3559945bb52103cf0eed64ea098f7b7f573f8d2962ef27b4b2/zarr-2.18.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fa/18/89ac62eac46b69948bf35fcd90d37103f38722968e2981f752d69081ec4d/zstandard-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: . docs: channels: @@ -318,56 +404,117 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - pypi: https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dd/e2/88e425adac5ad887a087c38d04fe2030010572a3e0e627f8a6e8c33eeda8/alembic-1.16.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/6a/885bc91484e1aa8f618f6f0228d76d0e67000b0fdd6090673b777e311913/asciitree-0.3.3.tar.gz + - pypi: https://files.pythonhosted.org/packages/d9/ab/6936e2663c47a926e0659437b9333ad87d1ff49b1375d239026e0a268eba/asgi_correlation_id-4.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fc/30/f7da949893bf0d8525acbaee256cf55430332852225f17e18a7f8783dd6f/bluesky-1.14.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/00/f0/2ef431fe4141f5e334759d73e81120492b23b2824336883a91ac04ba710b/cachetools-6.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/54/346f681c24a9c3a08e2e74dcee2555ccd1081705b46f791f7b228e177d06/canonicaljson-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e9/ad/0ce6cef4823d6c44b87e9b26b2cb284f6e25143ea0ab8215ac836bda825e/caproto-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/e8/64c37fadfc2816a7701fa8a6ed8d87327c7d54eacfbfb6edab14a2f2be75/cloudpickle-3.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-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/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ec/ef/3333295ed58d900a13c92806b67e62f27876845a9a908c939f040887cca9/cryptography-45.0.5-cp311-abi3-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/30/53b0844a7a4c6b041b111b24ca15cc9b8661a86fe1f6aaeb2d0d7f0fb1f2/dask-2025.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/11/79/479e2194c9096b92aecdf33634ae948d2be306c6011673e98ee1917f32c2/dpkt-1.9.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/a3/460c57f094a4a165c84a1341c373b0a4f5ec6ac244b998d5021aade89b77/ecdsa-0.19.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/35/a8/365059bbcd4572cbc41de17fd5b682be5868b218c3c5479071865cab9078/entrypoints-0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/f2/47e3f0e06c3bc3834db2a4ed01641e1ce493108461b9540dd5d2fb60d334/event_model-1.22.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/53/50/b1222562c6d270fea83e9c9075b8e8600b8479150a18e4516a6138b980d1/fastapi-0.115.14-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/bf/fd60001b3abc5222d8eaa4a204cd8c0ae78e75adc688f33ce4bf25b7fafa/fasteners-0.19-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/fe/5e/3be305568fe5f34448807976dc82fc151d76c3e0e03958f34770286278c1/flexparser-0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bb/61/78c7b3851add1481b048b5fdc29067397a1784e2910592bc81bb3f608635/fsspec-2025.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/26/f2/ad51331a157c7015c675702e2d5230c243695c788f8f75feba1af32b3617/greenlet-3.2.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fd/41/bc2df86b72965775f6d621e0ee269a5f3ac23e8f870abf519de9c7d93b4d/h5py-3.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_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/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/9d/0fb148dc4d6fa4a7dd1d8378168d9b4cd8d4560a6fbf6f0121c5fc34eb68/importlib_metadata-8.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/c1/fb2c66d92b5d0167c57042b784456ee3f8531a997726c88cf6f012a22da6/json_merge_patch-0.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/97/12/a1f2f4fdc6b7159c0d12249456f9fe454665b6126e98dbee9f2bd3cf735c/lz4-4.4.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2d/7b/2c1d74ca6c94f70a1add74a8393a0138172207dc5de6fc6269483519d048/msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.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/a6/91/86a6eac449ddfae239e93ffc1918cf33fd9bab35c04d1e963b311e347a73/netifaces-0.11.0.tar.gz - pypi: https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fa/91/d96999b41e3146b6c0ce6bddc5ad85803cb4d743c95394562c2a4bb8cded/numcodecs-0.15.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/aa/fc/ebfd32c3e124e6a1043e19c0ab0769818aa69050ce5589b63d05ff185526/numpy-2.2.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/12/f2/89ea3361a305466bc6460a532188830351220b5f0851a5fa133155c16eca/opentelemetry_api-1.32.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/5e/f5ded172af40b7e7ae60697a2b2096cec53e45344edce41e91a74425473c/ophyd-1.10.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/85/38/6b455b1d6f0b9aa3db4e8888b797f77d856a8350d836c66a43d1078573b2/ophyd_async-0.10.0a4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6d/4c/2bda09855c6b5f2c055034c9eda1529967b042ff8d81a05005115c4e6772/orjson-3.10.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/16/bd2f5904557265882108dc2e04f18abc05ab0c2b7082ae9430091daf1d5c/Pint-0.24.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/ae/ec06af4fe3ee72d16973474f122541746196aaa16cea6f66d18b963c6177/prometheus_client-0.22.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.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/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/30/05/ce271016e351fddc8399e546f6e23761967ee09c8c568bbfbecb0c150171/pytest_asyncio-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.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/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/05/7c/8abb70f9017a231c6c961a8941403ed6557664c0913e1bf413cbdc039e75/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_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/04/9e/91fe10ea09e2383f0d0531cf3da765fa01b3b9918e85df92081813259cf4/ruamel.yaml-0.18.12-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1f/8f/ecfbe2123ade605c49ef769788f79c38ddb1c8fa81e01f4dbf5cf1a44b16/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5e/51/5ba9ea3246ea068630acf35a6ba0d181e99f1af1afd17e159eac7e8bc2b8/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/37/63/383de8d9400a52d55683940387b160efb4f6b35bfb7645a443008780e2a9/tiled-0.1.0b29-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/03/98/eb27cc78ad3af8e302c9d8ff4977f5026676e130d28dd7578132a457170c/toolz-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/25/0d/4d564798a49bf5482a4fa9416dea6b6c0733a3b5700cb8a5a503c4b15853/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/5e/d8/9ffd8c237b3559945bb52103cf0eed64ea098f7b7f573f8d2962ef27b4b2/zarr-2.18.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl - pypi: . packages: @@ -392,6 +539,46 @@ packages: purls: [] size: 23621 timestamp: 1650670423406 +- pypi: https://files.pythonhosted.org/packages/d3/49/b5e260deff3d218a17fe23a1313bb3c033d846bf74505c297f74d2c8abfe/adbc_driver_manager-1.6.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: adbc-driver-manager + version: 1.6.0 + sha256: 41972465fa4db46bf151cc37000d0bd29c87c2eabbc81f502f0b6932c235f213 + requires_dist: + - typing-extensions + - pandas ; extra == 'dbapi' + - pyarrow>=14.0.1 ; extra == 'dbapi' + - duckdb ; extra == 'test' + - pandas ; extra == 'test' + - pyarrow>=14.0.1 ; extra == 'test' + - pytest ; extra == 'test' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/c1/45/2d3514fd2bf7ee7e1e28c45fdb70f2a27db2f8cc8af7e7c28f846dd7b99d/adbc_driver_postgresql-1.6.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: adbc-driver-postgresql + version: 1.6.0 + sha256: c3d09a688fb95bbe5431b488431145d77b3e20f8adbc413b1c2530b6623a9a2a + requires_dist: + - adbc-driver-manager + - importlib-resources>=1.3 + - pandas ; extra == 'dbapi' + - pyarrow>=14.0.1 ; extra == 'dbapi' + - pandas ; extra == 'test' + - polars ; extra == 'test' + - pyarrow>=14.0.1 ; extra == 'test' + - pytest ; extra == 'test' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/92/ec/ce41f3c84b5b4f762e4de9072574714c623e959924bf1bbaf1548612cb6e/adbc_driver_sqlite-1.6.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: adbc-driver-sqlite + version: 1.6.0 + sha256: 922a561c84c53f8fac42f1d636dfb915a265de3ebd39c35cf0d1cbe58d668616 + requires_dist: + - adbc-driver-manager + - importlib-resources>=1.3 + - pandas ; extra == 'dbapi' + - pyarrow>=14.0.1 ; extra == 'dbapi' + - pandas ; extra == 'test' + - pyarrow>=14.0.1 ; extra == 'test' + - pytest ; extra == 'test' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/5b/08/185c3b29b0698328b202e6c965c23187e2e29ead78cb468aab0a09ee97fc/aioca-1.8.1-py3-none-any.whl name: aioca version: 1.8.1 @@ -416,11 +603,46 @@ packages: - tox-direct ; extra == 'dev' - types-mock ; extra == 'dev' requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl + name: aiofiles + version: 24.1.0 + sha256: b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl + name: aiosqlite + version: 0.21.0 + sha256: 2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0 + requires_dist: + - typing-extensions>=4.0 + - attribution==1.7.1 ; extra == 'dev' + - black==24.3.0 ; extra == 'dev' + - build>=1.2 ; extra == 'dev' + - coverage[toml]==7.6.10 ; extra == 'dev' + - flake8==7.0.0 ; extra == 'dev' + - flake8-bugbear==24.12.12 ; extra == 'dev' + - flit==3.10.1 ; extra == 'dev' + - mypy==1.14.1 ; extra == 'dev' + - ufmt==2.5.1 ; extra == 'dev' + - usort==1.0.8.post1 ; extra == 'dev' + - sphinx==8.1.3 ; extra == 'docs' + - sphinx-mdinclude==0.6.1 ; extra == 'docs' + requires_python: '>=3.9' - 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/dd/e2/88e425adac5ad887a087c38d04fe2030010572a3e0e627f8a6e8c33eeda8/alembic-1.16.2-py3-none-any.whl + name: alembic + version: 1.16.2 + sha256: 5f42e9bd0afdbd1d5e3ad856c01754530367debdebf21ed6894e34af52b3bb03 + requires_dist: + - sqlalchemy>=1.4.0 + - mako + - typing-extensions>=4.12 + - tomli ; python_full_version < '3.11' + - tzdata ; extra == 'tz' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl name: annotated-types version: 0.7.0 @@ -428,6 +650,44 @@ packages: requires_dist: - typing-extensions>=4.0.0 ; python_full_version < '3.9' requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl + name: anyio + version: 4.9.0 + sha256: 9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c + requires_dist: + - exceptiongroup>=1.0.2 ; python_full_version < '3.11' + - idna>=2.8 + - sniffio>=1.1 + - typing-extensions>=4.5 ; python_full_version < '3.13' + - trio>=0.26.1 ; extra == 'trio' + - anyio[trio] ; extra == 'test' + - blockbuster>=1.5.23 ; extra == 'test' + - coverage[toml]>=7 ; extra == 'test' + - exceptiongroup>=1.2.0 ; extra == 'test' + - hypothesis>=4.0 ; extra == 'test' + - psutil>=5.9 ; extra == 'test' + - pytest>=7.0 ; extra == 'test' + - trustme ; extra == 'test' + - truststore>=0.9.1 ; python_full_version >= '3.10' and extra == 'test' + - uvloop>=0.21 ; python_full_version < '3.14' and platform_python_implementation == 'CPython' and sys_platform != 'win32' and extra == 'test' + - packaging ; extra == 'doc' + - sphinx~=8.2 ; extra == 'doc' + - sphinx-rtd-theme ; extra == 'doc' + - sphinx-autodoc-typehints>=1.2.0 ; extra == 'doc' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/2d/6a/885bc91484e1aa8f618f6f0228d76d0e67000b0fdd6090673b777e311913/asciitree-0.3.3.tar.gz + name: asciitree + version: 0.3.3 + sha256: 4aa4b9b649f85e3fcb343363d97564aa1fb62e249677f2e18a96765145cc0f6e +- pypi: https://files.pythonhosted.org/packages/d9/ab/6936e2663c47a926e0659437b9333ad87d1ff49b1375d239026e0a268eba/asgi_correlation_id-4.3.4-py3-none-any.whl + name: asgi-correlation-id + version: 4.3.4 + sha256: 36ce69b06c7d96b4acb89c7556a4c4f01a972463d3d49c675026cbbd08e9a0a2 + requires_dist: + - celery ; extra == 'celery' + - packaging + - starlette>=0.18 + requires_python: '>=3.8,<4.0' - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl name: asttokens version: 3.0.0 @@ -439,6 +699,25 @@ packages: - pytest-cov ; extra == 'test' - pytest-xdist ; extra == 'test' requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/96/19/139227a6e67f407b9c386cb594d9628c6c78c9024f26df87c912fabd4368/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: asyncpg + version: 0.30.0 + sha256: 46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4 + requires_dist: + - async-timeout>=4.0.3 ; python_full_version < '3.11' + - sphinx~=8.1.3 ; extra == 'docs' + - sphinx-rtd-theme>=1.2.2 ; extra == 'docs' + - gssapi ; sys_platform != 'win32' and extra == 'gssauth' + - sspilib ; sys_platform == 'win32' and extra == 'gssauth' + - flake8~=6.1 ; extra == 'test' + - flake8-pyi~=24.1.0 ; extra == 'test' + - distro~=1.9.0 ; extra == 'test' + - mypy~=1.8.0 ; extra == 'test' + - uvloop>=0.15.3 ; python_full_version < '3.14' and sys_platform != 'win32' and extra == 'test' + - gssapi ; sys_platform == 'linux' and extra == 'test' + - k5test ; sys_platform == 'linux' and extra == 'test' + - sspilib ; sys_platform == 'win32' and extra == 'test' + requires_python: '>=3.8.0' - pypi: https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl name: attrs version: 25.3.0 @@ -485,6 +764,25 @@ packages: - mypy>=1.11.1 ; python_full_version >= '3.10' and platform_python_implementation == 'CPython' and extra == 'tests-mypy' - pytest-mypy-plugins ; python_full_version >= '3.10' and platform_python_implementation == 'CPython' and extra == 'tests-mypy' requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/d2/af/92435ec3cfe35bd738a3ba2332c8cbdb50f0cc66d1f9943a1a457990fcd6/awkward-2.8.4-py3-none-any.whl + name: awkward + version: 2.8.4 + sha256: 6889e632497be712c2515a15947a352ed3752729c1311802722b432f7db95a5d + requires_dist: + - awkward-cpp==46 + - fsspec>=2022.11.0 + - importlib-metadata>=4.13.0 ; python_full_version < '3.12' + - numpy>=1.18.0 + - packaging + - typing-extensions>=4.1.0 ; python_full_version < '3.11' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/2b/cf/9424352bd9568bd56eb88e5fc8400a6c5bd3c827fdeeae202fcaaf3c1910/awkward_cpp-46-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: awkward-cpp + version: '46' + sha256: 53180c40c05ffb3f1671a9cde59dde34b0cc49b6089aa6774a4067f14dbf08d2 + requires_dist: + - numpy>=1.18.0 + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl name: babel version: 2.17.0 @@ -513,6 +811,42 @@ packages: - html5lib ; extra == 'html5lib' - lxml ; extra == 'lxml' requires_python: '>=3.7.0' +- pypi: https://files.pythonhosted.org/packages/c9/38/8815054e054e45fda3c9d1e25ac77e6387d10d8e33c64bcf79f432b3c4fa/blosc2-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: blosc2 + version: 3.5.1 + sha256: f88cfa1f1d64d77278c7fae0ab8c42291b7b2e8f36fcee154a0c55388e697fe9 + requires_dist: + - numpy>=1.26 + - ndindex + - msgpack + - platformdirs + - numexpr ; platform_machine != 'wasm32' + - py-cpuinfo ; platform_machine != 'wasm32' + - requests + - dask ; extra == 'dev' + - h5py ; extra == 'dev' + - hdf5plugin ; extra == 'dev' + - jupyterlab ; extra == 'dev' + - matplotlib ; extra == 'dev' + - pandas ; extra == 'dev' + - plotly ; extra == 'dev' + - pre-commit ; extra == 'dev' + - pyarrow ; extra == 'dev' + - ruff ; extra == 'dev' + - s3fs ; extra == 'dev' + - xarray ; extra == 'dev' + - zarr ; extra == 'dev' + - pytest ; extra == 'test' + - psutil ; platform_machine != 'wasm32' and extra == 'test' + - torch ; platform_machine != 'wasm32' and extra == 'test' + - sphinx>=8 ; extra == 'doc' + - pydata-sphinx-theme ; extra == 'doc' + - numpydoc ; extra == 'doc' + - myst-parser ; extra == 'doc' + - sphinx-paramlinks ; extra == 'doc' + - nbsphinx ; extra == 'doc' + - sphinx-panels ; extra == 'doc' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/fc/30/f7da949893bf0d8525acbaee256cf55430332852225f17e18a7f8783dd6f/bluesky-1.14.1-py3-none-any.whl name: bluesky version: 1.14.1 @@ -617,6 +951,16 @@ packages: purls: [] size: 158834 timestamp: 1745260472197 +- pypi: https://files.pythonhosted.org/packages/00/f0/2ef431fe4141f5e334759d73e81120492b23b2824336883a91ac04ba710b/cachetools-6.1.0-py3-none-any.whl + name: cachetools + version: 6.1.0 + sha256: 1c7bb3cf9193deaf3508b7c5f2a79986c13ea38965c5adcff1f84519cf39163e + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/e5/54/346f681c24a9c3a08e2e74dcee2555ccd1081705b46f791f7b228e177d06/canonicaljson-2.0.0-py3-none-any.whl + name: canonicaljson + version: 2.0.0 + sha256: c38a315de3b5a0532f1ec1f9153cd3d716abfc565a558d00a4835428a34fca5b + requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/e9/ad/0ce6cef4823d6c44b87e9b26b2cb284f6e25143ea0ab8215ac836bda825e/caproto-1.1.1-py3-none-any.whl name: caproto version: 1.1.1 @@ -652,16 +996,19 @@ packages: requires_python: '>=3.8' - pypi: . name: cditools - version: 0.1.dev25+gcec0d98.d20250630 - sha256: 786ea3d4db672c2c44cf9dd24352a746e6d16ab8e8c1541eb9ec9c027abd77cc + version: 0.1.dev35+gd6e4bc7.d20250702 + sha256: a9151703cfcc98e3ec8b42f6f48a3d0b846f2cbe302adfad988cb1c3e94ab350 requires_dist: - ophyd - ophyd-async>=0.10.0a4 - h5py + - entrypoints ; extra == 'test' - pytest>=6 ; extra == 'test' - pytest-cov>=3 ; extra == 'test' - pytest-asyncio>=0.18 ; extra == 'test' - caproto[standard]>=0.4.2rc1,!=1.2.0 ; extra == 'test' + - tiled[minimal-client] ; extra == 'test' + - tiled[minimal-server] ; extra == 'test' - caproto[standard]>=0.4.2rc1,!=1.2.0 ; extra == 'dev' - pytest>=6 ; extra == 'dev' - pytest-cov>=3 ; extra == 'dev' @@ -671,6 +1018,7 @@ packages: - ipython ; extra == 'dev' - ruff ; extra == 'dev' - aioca ; extra == 'dev' + - tiled[minimal-client,server] ; extra == 'dev' - sphinx>=7.0 ; extra == 'docs' - myst-parser>=0.13 ; extra == 'docs' - sphinx-copybutton ; extra == 'docs' @@ -683,6 +1031,13 @@ packages: version: 2025.1.31 sha256: ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: cffi + version: 1.17.1 + sha256: dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd + requires_dist: + - pycparser + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl name: cfgv version: 3.4.0 @@ -693,6 +1048,18 @@ packages: version: 3.4.1 sha256: 955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11 requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl + name: click + version: 8.2.1 + sha256: 61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b + requires_dist: + - colorama ; sys_platform == 'win32' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/7e/e8/64c37fadfc2816a7701fa8a6ed8d87327c7d54eacfbfb6edab14a2f2be75/cloudpickle-3.1.1-py3-none-any.whl + name: cloudpickle + version: 3.1.1 + sha256: c8c5a44295039331ee9dad40ba100a9c7297b6f988e50e87ccdf3765a668350e + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-py3-none-any.whl name: colorlog version: 6.9.0 @@ -724,6 +1091,35 @@ packages: requires_dist: - tomli ; python_full_version <= '3.11' and extra == 'toml' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/ec/ef/3333295ed58d900a13c92806b67e62f27876845a9a908c939f040887cca9/cryptography-45.0.5-cp311-abi3-manylinux_2_28_x86_64.whl + name: cryptography + version: 45.0.5 + sha256: dd420e577921c8c2d31289536c386aaa30140b473835e97f83bc71ea9d2baf2d + requires_dist: + - cffi>=1.14 ; platform_python_implementation != 'PyPy' + - bcrypt>=3.1.5 ; extra == 'ssh' + - nox>=2024.4.15 ; extra == 'nox' + - nox[uv]>=2024.3.2 ; python_full_version >= '3.8' and extra == 'nox' + - cryptography-vectors==45.0.5 ; extra == 'test' + - pytest>=7.4.0 ; extra == 'test' + - pytest-benchmark>=4.0 ; extra == 'test' + - pytest-cov>=2.10.1 ; extra == 'test' + - pytest-xdist>=3.5.0 ; extra == 'test' + - pretend>=0.7 ; extra == 'test' + - certifi>=2024 ; extra == 'test' + - pytest-randomly ; extra == 'test-randomorder' + - sphinx>=5.3.0 ; extra == 'docs' + - sphinx-rtd-theme>=3.0.0 ; python_full_version >= '3.8' and extra == 'docs' + - sphinx-inline-tabs ; python_full_version >= '3.8' and extra == 'docs' + - pyenchant>=3 ; extra == 'docstest' + - readme-renderer>=30.0 ; extra == 'docstest' + - sphinxcontrib-spelling>=7.3.1 ; extra == 'docstest' + - build>=1.0.0 ; extra == 'sdist' + - ruff>=0.3.6 ; extra == 'pep8test' + - mypy>=1.4 ; extra == 'pep8test' + - check-sdist ; python_full_version >= '3.8' and extra == 'pep8test' + - click>=8.0.1 ; extra == 'pep8test' + requires_python: '>=3.7,!=3.9.0,!=3.9.1' - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl name: cycler version: 0.12.1 @@ -737,6 +1133,38 @@ packages: - pytest-cov ; extra == 'tests' - pytest-xdist ; extra == 'tests' requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/38/30/53b0844a7a4c6b041b111b24ca15cc9b8661a86fe1f6aaeb2d0d7f0fb1f2/dask-2025.5.1-py3-none-any.whl + name: dask + version: 2025.5.1 + sha256: 3b85fdaa5f6f989dde49da6008415b1ae996985ebdfb1e40de2c997d9010371d + requires_dist: + - click>=8.1 + - cloudpickle>=3.0.0 + - fsspec>=2021.9.0 + - packaging>=20.0 + - partd>=1.4.0 + - pyyaml>=5.3.1 + - toolz>=0.10.0 + - importlib-metadata>=4.13.0 ; python_full_version < '3.12' + - numpy>=1.24 ; extra == 'array' + - dask[array] ; extra == 'dataframe' + - pandas>=2.0 ; extra == 'dataframe' + - pyarrow>=14.0.1 ; extra == 'dataframe' + - distributed==2025.5.1 ; extra == 'distributed' + - bokeh>=3.1.0 ; extra == 'diagnostics' + - jinja2>=2.10.3 ; extra == 'diagnostics' + - dask[array,dataframe,diagnostics,distributed] ; extra == 'complete' + - pyarrow>=14.0.1 ; extra == 'complete' + - lz4>=4.3.2 ; extra == 'complete' + - pandas[test] ; extra == 'test' + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-mock ; extra == 'test' + - pytest-rerunfailures ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest-xdist ; extra == 'test' + - pre-commit ; extra == 'test' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl name: decorator version: 5.2.1 @@ -767,6 +1195,25 @@ packages: name: dpkt version: 1.9.8 sha256: 4da4d111d7bf67575b571f5c678c71bddd2d8a01a3d57d489faf0a92c748fbfd +- pypi: https://files.pythonhosted.org/packages/90/a6/c8577b741974f106e24f8eb3efedc399be1a23cbbdcf49dd4bea5bb8aa4e/duckdb-1.3.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: duckdb + version: 1.3.1 + sha256: 8793b5abb365bbbf64ba3065f3a37951fe04f2d4506b0e24f3f8ecd08b3af4ba + requires_python: '>=3.7.0' +- pypi: https://files.pythonhosted.org/packages/cb/a3/460c57f094a4a165c84a1341c373b0a4f5ec6ac244b998d5021aade89b77/ecdsa-0.19.1-py2.py3-none-any.whl + name: ecdsa + version: 0.19.1 + sha256: 30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3 + 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/35/a8/365059bbcd4572cbc41de17fd5b682be5868b218c3c5479071865cab9078/entrypoints-0.4-py3-none-any.whl + name: entrypoints + version: '0.4' + sha256: f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f + requires_python: '>=3.6' - pypi: https://files.pythonhosted.org/packages/4a/18/3853de30cc92c164eccff6e62599ef88d0aa3b64740973b63e13f88145de/epicscorelibs-7.0.7.99.1.1.tar.gz name: epicscorelibs version: 7.0.7.99.1.1 @@ -776,6 +1223,11 @@ packages: - setuptools-dso>=2.11a2 - numpy requires_python: '>=2.7' +- 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/b1/f2/47e3f0e06c3bc3834db2a4ed01641e1ce493108461b9540dd5d2fb60d334/event_model-1.22.3-py3-none-any.whl name: event-model version: 1.22.3 @@ -817,6 +1269,38 @@ packages: - littleutils ; extra == 'tests' - rich ; python_full_version >= '3.11' and extra == 'tests' requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/53/50/b1222562c6d270fea83e9c9075b8e8600b8479150a18e4516a6138b980d1/fastapi-0.115.14-py3-none-any.whl + name: fastapi + version: 0.115.14 + sha256: 6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca + requires_dist: + - starlette>=0.40.0,<0.47.0 + - pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0 + - typing-extensions>=4.8.0 + - fastapi-cli[standard]>=0.0.5 ; extra == 'standard' + - httpx>=0.23.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' + - fastapi-cli[standard]>=0.0.5 ; extra == 'all' + - httpx>=0.23.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' + - ujson>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0 ; extra == 'all' + - orjson>=3.2.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.8' +- pypi: https://files.pythonhosted.org/packages/61/bf/fd60001b3abc5222d8eaa4a204cd8c0ae78e75adc688f33ce4bf25b7fafa/fasteners-0.19-py3-none-any.whl + name: fasteners + version: '0.19' + sha256: 758819cb5d94cdedf4e836988b74de396ceacb8e2794d21f82d131fd9ee77237 + requires_python: '>=3.6' - pypi: https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl name: filelock version: 3.18.0 @@ -858,6 +1342,113 @@ packages: - pytest-cov ; extra == 'test' - pytest-subtests ; extra == 'test' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/bb/61/78c7b3851add1481b048b5fdc29067397a1784e2910592bc81bb3f608635/fsspec-2025.5.1-py3-none-any.whl + name: fsspec + version: 2025.5.1 + sha256: 24d3a2e663d5fc735ab256263c4075f374a174c3410c0b25e5bd1970bceaa462 + requires_dist: + - adlfs ; extra == 'abfs' + - adlfs ; extra == 'adl' + - pyarrow>=1 ; extra == 'arrow' + - dask ; extra == 'dask' + - distributed ; extra == 'dask' + - pre-commit ; extra == 'dev' + - ruff ; extra == 'dev' + - numpydoc ; extra == 'doc' + - sphinx ; extra == 'doc' + - sphinx-design ; extra == 'doc' + - sphinx-rtd-theme ; extra == 'doc' + - yarl ; extra == 'doc' + - dropbox ; extra == 'dropbox' + - dropboxdrivefs ; extra == 'dropbox' + - requests ; extra == 'dropbox' + - adlfs ; extra == 'full' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'full' + - dask ; extra == 'full' + - distributed ; extra == 'full' + - dropbox ; extra == 'full' + - dropboxdrivefs ; extra == 'full' + - fusepy ; extra == 'full' + - gcsfs ; extra == 'full' + - libarchive-c ; extra == 'full' + - ocifs ; extra == 'full' + - panel ; extra == 'full' + - paramiko ; extra == 'full' + - pyarrow>=1 ; extra == 'full' + - pygit2 ; extra == 'full' + - requests ; extra == 'full' + - s3fs ; extra == 'full' + - smbprotocol ; extra == 'full' + - tqdm ; extra == 'full' + - fusepy ; extra == 'fuse' + - gcsfs ; extra == 'gcs' + - pygit2 ; extra == 'git' + - requests ; extra == 'github' + - gcsfs ; extra == 'gs' + - panel ; extra == 'gui' + - pyarrow>=1 ; extra == 'hdfs' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'http' + - libarchive-c ; extra == 'libarchive' + - ocifs ; extra == 'oci' + - s3fs ; extra == 's3' + - paramiko ; extra == 'sftp' + - smbprotocol ; extra == 'smb' + - paramiko ; extra == 'ssh' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'test' + - numpy ; extra == 'test' + - pytest ; extra == 'test' + - pytest-asyncio!=0.22.0 ; extra == 'test' + - pytest-benchmark ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-mock ; extra == 'test' + - pytest-recording ; extra == 'test' + - pytest-rerunfailures ; extra == 'test' + - requests ; extra == 'test' + - aiobotocore>=2.5.4,<3.0.0 ; extra == 'test-downstream' + - dask[dataframe,test] ; extra == 'test-downstream' + - moto[server]>4,<5 ; extra == 'test-downstream' + - pytest-timeout ; extra == 'test-downstream' + - xarray ; extra == 'test-downstream' + - adlfs ; extra == 'test-full' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'test-full' + - cloudpickle ; extra == 'test-full' + - dask ; extra == 'test-full' + - distributed ; extra == 'test-full' + - dropbox ; extra == 'test-full' + - dropboxdrivefs ; extra == 'test-full' + - fastparquet ; extra == 'test-full' + - fusepy ; extra == 'test-full' + - gcsfs ; extra == 'test-full' + - jinja2 ; extra == 'test-full' + - kerchunk ; extra == 'test-full' + - libarchive-c ; extra == 'test-full' + - lz4 ; extra == 'test-full' + - notebook ; extra == 'test-full' + - numpy ; extra == 'test-full' + - ocifs ; extra == 'test-full' + - pandas ; extra == 'test-full' + - panel ; extra == 'test-full' + - paramiko ; extra == 'test-full' + - pyarrow ; extra == 'test-full' + - pyarrow>=1 ; extra == 'test-full' + - pyftpdlib ; extra == 'test-full' + - pygit2 ; extra == 'test-full' + - pytest ; extra == 'test-full' + - pytest-asyncio!=0.22.0 ; extra == 'test-full' + - pytest-benchmark ; extra == 'test-full' + - pytest-cov ; extra == 'test-full' + - pytest-mock ; extra == 'test-full' + - pytest-recording ; extra == 'test-full' + - pytest-rerunfailures ; extra == 'test-full' + - python-snappy ; extra == 'test-full' + - requests ; extra == 'test-full' + - smbprotocol ; extra == 'test-full' + - tqdm ; extra == 'test-full' + - urllib3 ; extra == 'test-full' + - zarr ; extra == 'test-full' + - zstandard ; extra == 'test-full' + - tqdm ; extra == 'tqdm' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/27/48/e791a7ed487dbb9729ef32bb5d1af16693d8925f4366befef54119b2e576/furo-2024.8.6-py3-none-any.whl name: furo version: 2024.8.6 @@ -868,6 +1459,31 @@ packages: - sphinx-basic-ng>=1.0.0b2 - pygments>=2.7 requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/26/f2/ad51331a157c7015c675702e2d5230c243695c788f8f75feba1af32b3617/greenlet-3.2.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + name: greenlet + version: 3.2.3 + sha256: 2c724620a101f8170065d7dded3f962a2aea7a7dae133a009cada42847e04a7b + requires_dist: + - sphinx ; extra == 'docs' + - furo ; extra == 'docs' + - objgraph ; extra == 'test' + - psutil ; extra == 'test' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl + name: h11 + version: 0.16.0 + sha256: 63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/4e/27/ca28b77f4cf613282e4ea9bac360cdfa8db8b4d2154e2f0bd82fd26baeb3/h5netcdf-1.6.3-py3-none-any.whl + name: h5netcdf + version: 1.6.3 + sha256: b79bc24d1f8b1cdc1f16b213753209411e08af04e35b0bcf5ce5cf7fb4023572 + requires_dist: + - h5py + - packaging + - netcdf4 ; extra == 'test' + - pytest ; extra == 'test' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/fd/41/bc2df86b72965775f6d621e0ee269a5f3ac23e8f870abf519de9c7d93b4d/h5py-3.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl name: h5py version: 3.13.0 @@ -875,10 +1491,60 @@ packages: requires_dist: - numpy>=1.19.3 requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/ba/db/8d85bd3f5b9ee894518f9d9ed1019fd8b7014340beca512dfb76d5632ec9/hdf5plugin-5.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: hdf5plugin + version: 5.1.0 + sha256: 6da81b0b168f271b0cf995a12c28cf01b381587fed21f25fd91b2c90d5108425 + requires_dist: + - h5py>=3.0.0 + - ipython ; extra == 'doc' + - nbsphinx ; extra == 'doc' + - sphinx ; extra == 'doc' + - sphinx-rtd-theme ; extra == 'doc' + - blosc2>=2.5.1 ; python_full_version >= '3.9' and extra == 'test' + - blosc2-grok>=0.2.2 ; python_full_version >= '3.9' and extra == 'test' + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/fe/47/deb64c73aec25af7699247e021153a6bfe9a08452f7f7337dcee4aa07a2b/historydict-1.2.6-py3-none-any.whl name: historydict version: 1.2.6 sha256: b4b00a170f05502aa682caba62435da5fe1f73037e884707581fe84f8d7b43f5 +- pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl + name: httpcore + version: 1.0.9 + sha256: 2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 + requires_dist: + - certifi + - h11>=0.16 + - anyio>=4.0,<5.0 ; extra == 'asyncio' + - h2>=3,<5 ; extra == 'http2' + - socksio==1.* ; extra == 'socks' + - trio>=0.22.0,<1.0 ; extra == 'trio' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: httptools + version: 0.6.4 + sha256: 4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071 + requires_dist: + - cython>=0.29.24 ; extra == 'test' + requires_python: '>=3.8.0' +- pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl + name: httpx + version: 0.28.1 + sha256: d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad + requires_dist: + - anyio + - certifi + - httpcore==1.* + - idna + - brotli ; platform_python_implementation == 'CPython' and extra == 'brotli' + - brotlicffi ; platform_python_implementation != 'CPython' and extra == 'brotli' + - click==8.* ; extra == 'cli' + - pygments==2.* ; extra == 'cli' + - rich>=10,<14 ; extra == 'cli' + - h2>=3,<5 ; extra == 'http2' + - socksio==1.* ; extra == 'socks' + - zstandard>=0.18.0 ; extra == 'zstd' + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/2b/d3/85feeba1d097b81a44bcffa6a0beab7b4dfffe78e82fc54978d3ac380736/identify-2.6.10-py2.py3-none-any.whl name: identify version: 2.6.10 @@ -1054,6 +1720,28 @@ packages: - markupsafe>=2.0 - babel>=2.7 ; extra == 'i18n' requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl + name: jmespath + version: 1.0.1 + sha256: 02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/cb/c1/fb2c66d92b5d0167c57042b784456ee3f8531a997726c88cf6f012a22da6/json_merge_patch-0.3.0-py3-none-any.whl + name: json-merge-patch + version: 0.3.0 + sha256: e0a593719b293ff63858ecaae3afbcb4ff0b57f785453c6783d7b0c3e2708b76 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl + name: jsonpatch + version: '1.33' + sha256: 0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade + requires_dist: + - jsonpointer>=1.9 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*' +- pypi: https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl + name: jsonpointer + version: 3.0.0 + sha256: 13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942 + requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl name: jsonschema version: 4.24.0 @@ -1214,6 +1902,16 @@ packages: purls: [] size: 60963 timestamp: 1727963148474 +- pypi: https://files.pythonhosted.org/packages/d2/1b/656f5a357de7135a3777bd735cc7c9b8f23b4d37465505bd0eaf4be9befe/llvmlite-0.44.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: llvmlite + version: 0.44.0 + sha256: 46224058b13c96af1365290bdfebe9a6264ae62fb79b2b55693deed11657a8bf + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + name: locket + version: 1.0.0 + sha256: b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*' - pypi: https://files.pythonhosted.org/packages/97/12/a1f2f4fdc6b7159c0d12249456f9fe454665b6126e98dbee9f2bd3cf735c/lz4-4.4.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl name: lz4 version: 4.4.4 @@ -1226,6 +1924,16 @@ packages: - sphinx-bootstrap-theme ; extra == 'docs' - flake8 ; extra == 'flake8' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl + name: mako + version: 1.3.10 + sha256: baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59 + requires_dist: + - markupsafe>=0.9.2 + - pytest ; extra == 'testing' + - babel ; extra == 'babel' + - lingua ; extra == 'lingua' + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl name: markdown-it-py version: 3.0.0 @@ -1347,6 +2055,13 @@ packages: purls: [] size: 891641 timestamp: 1738195959188 +- pypi: https://files.pythonhosted.org/packages/b5/07/c64b0c8416f604f6990da5d1fa97c9de1278a4eec1efcc63b71053b4f0c0/ndindex-1.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: ndindex + version: 1.10.0 + sha256: 38a56a16edbd62ef039b93e393047e66238d02dbc1e95e95b79c0bdd0a4785f7 + requires_dist: + - numpy ; extra == 'arrays' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/a6/91/86a6eac449ddfae239e93ffc1918cf33fd9bab35c04d1e963b311e347a73/netifaces-0.11.0.tar.gz name: netifaces version: 0.11.0 @@ -1391,11 +2106,53 @@ packages: version: 1.9.1 sha256: ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9 requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*' +- pypi: https://files.pythonhosted.org/packages/0d/e0/5ea04e7ad2c39288c0f0f9e8d47638ad70f28e275d092733b5817cf243c9/numba-0.61.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: numba + version: 0.61.2 + sha256: bdbca73ad81fa196bd53dc12e3aaf1564ae036e0c125f237c7644fe64a4928ab + requires_dist: + - llvmlite>=0.44.0.dev0,<0.45 + - numpy>=1.24,<2.3 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/fa/91/d96999b41e3146b6c0ce6bddc5ad85803cb4d743c95394562c2a4bb8cded/numcodecs-0.15.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: numcodecs + version: 0.15.1 + sha256: 1dfdea4a67108205edfce99c1cb6cd621343bc7abb7e16a041c966776920e7de + requires_dist: + - numpy>=1.24 + - deprecated + - sphinx ; extra == 'docs' + - sphinx-issues ; extra == 'docs' + - pydata-sphinx-theme ; extra == 'docs' + - numpydoc ; extra == 'docs' + - coverage ; extra == 'test' + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' + - importlib-metadata ; extra == 'test-extras' + - msgpack ; extra == 'msgpack' + - zfpy>=1.0.0 ; extra == 'zfpy' + - pcodec>=0.3,<0.4 ; extra == 'pcodec' + - crc32c>=2.7 ; extra == 'crc32c' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/b9/7d/4911f40d3610fc5557029f0d1f20ef9f571488319567ac4d8ee6d0978ee6/numexpr-2.11.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: numexpr + version: 2.11.0 + sha256: a1719788a787808c15c9bb98b6ff0c97d64a0e59c1a6ebe36d4ae4d7c5c09b95 + requires_dist: + - numpy>=1.23.0 + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/aa/fc/ebfd32c3e124e6a1043e19c0ab0769818aa69050ce5589b63d05ff185526/numpy-2.2.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl name: numpy version: 2.2.5 sha256: 2ba321813a00e508d5421104464510cc962a6f791aa2fca1c97b1e65027da80d requires_python: '>=3.10' +- 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' - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.0-h7b32b05_0.conda sha256: 38285d280f84f1755b7c54baf17eccf2e3e696287954ce0adca16546b85ee62c md5: bb539841f2a3fde210f387d00ed4bb9d @@ -1512,6 +2269,11 @@ packages: - types-mock ; extra == 'dev' - types-pyyaml ; extra == 'dev' requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/6d/4c/2bda09855c6b5f2c055034c9eda1529967b042ff8d81a05005115c4e6772/orjson-3.10.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: orjson + version: 3.10.18 + sha256: bb70d489bc79b7519e5803e2cc4c72343c9dc1154258adf2f8925d0b60da7c58 + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl name: packaging version: '25.0' @@ -1619,12 +2381,54 @@ packages: - docopt ; extra == 'testing' - pytest ; extra == 'testing' requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl + name: partd + version: 1.4.2 + sha256: 978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f + requires_dist: + - locket + - toolz + - numpy>=1.20.0 ; extra == 'complete' + - pandas>=1.3 ; extra == 'complete' + - pyzmq ; extra == 'complete' + - blosc ; extra == 'complete' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl name: pexpect version: 4.9.0 sha256: 7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523 requires_dist: - ptyprocess>=0.5 +- pypi: https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: pillow + version: 11.3.0 + sha256: 13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8 + requires_dist: + - furo ; extra == 'docs' + - olefile ; extra == 'docs' + - sphinx>=8.2 ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - sphinxext-opengraph ; extra == 'docs' + - olefile ; extra == 'fpx' + - olefile ; extra == 'mic' + - pyarrow ; extra == 'test-arrow' + - check-manifest ; extra == 'tests' + - coverage>=7.4.2 ; extra == 'tests' + - defusedxml ; extra == 'tests' + - markdown2 ; extra == 'tests' + - olefile ; extra == 'tests' + - packaging ; extra == 'tests' + - pyroma ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-timeout ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + - trove-classifiers>=2024.10.12 ; extra == 'tests' + - typing-extensions ; python_full_version < '3.10' and extra == 'typing' + - defusedxml ; extra == 'xmp' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/b7/16/bd2f5904557265882108dc2e04f18abc05ab0c2b7082ae9430091daf1d5c/Pint-0.24.4-py3-none-any.whl name: pint version: 0.24.4 @@ -1690,6 +2494,13 @@ packages: - pyyaml>=5.1 - virtualenv>=20.10.0 requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/32/ae/ec06af4fe3ee72d16973474f122541746196aaa16cea6f66d18b963c6177/prometheus_client-0.22.1-py3-none-any.whl + name: prometheus-client + version: 0.22.1 + sha256: cca895342e308174341b2cbf99a56bef291fbc0ef7b9e5412a0f26d653ba7094 + requires_dist: + - twisted ; extra == 'twisted' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl name: prompt-toolkit version: 3.0.51 @@ -1707,6 +2518,31 @@ packages: sha256: 1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0 requires_dist: - pytest ; extra == 'tests' +- pypi: https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl + name: py-cpuinfo + version: 9.0.0 + sha256: 859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5 +- pypi: https://files.pythonhosted.org/packages/be/27/43a47fa0ff9053ab5203bb3faeec435d43c0d8bfa40179bfd076cdbd4e1c/pyarrow-20.0.0-cp313-cp313-manylinux_2_28_x86_64.whl + name: pyarrow + version: 20.0.0 + sha256: 97c8dc984ed09cb07d618d57d8d4b67a5100a30c3818c2fb0b04599f0da2de7b + requires_dist: + - pytest ; extra == 'test' + - hypothesis ; extra == 'test' + - cffi ; extra == 'test' + - pytz ; extra == 'test' + - pandas ; extra == 'test' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl + name: pyasn1 + version: 0.6.1 + sha256: 0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl + name: pycparser + version: '2.22' + sha256: c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl name: pydantic version: 2.11.5 @@ -1737,6 +2573,22 @@ 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/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl + name: pydantic-settings + version: 2.10.1 + sha256: a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796 + requires_dist: + - pydantic>=2.7.0 + - python-dotenv>=0.21.0 + - typing-inspection>=0.4.0 + - boto3-stubs[secretsmanager] ; extra == 'aws-secrets-manager' + - boto3>=1.35.0 ; extra == 'aws-secrets-manager' + - azure-identity>=1.16.0 ; extra == 'azure-key-vault' + - azure-keyvault-secrets>=4.8.0 ; extra == 'azure-key-vault' + - google-cloud-secret-manager>=2.23.1 ; extra == 'gcp-secret-manager' + - tomli>=2.0.1 ; extra == 'toml' + - pyyaml>=6.0.1 ; extra == 'yaml' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl name: pygments version: 2.19.1 @@ -1823,6 +2675,32 @@ packages: requires_dist: - six>=1.5 requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*' +- pypi: https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl + name: python-dotenv + version: 1.1.1 + sha256: 31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc + requires_dist: + - click>=5.0 ; extra == 'cli' + requires_python: '>=3.9' +- 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/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl + name: python-multipart + version: 0.0.20 + sha256: 8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104 + requires_python: '>=3.8' - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-7_cp313.conda build_number: 7 sha256: 0595134584589064f56e67d3de1d8fcbb673a972946bce25fb593fb092fdcd97 @@ -1875,6 +2753,16 @@ packages: - pysocks>=1.5.6,!=1.5.7 ; extra == 'socks' - chardet>=3.0.2,<6 ; extra == 'use-chardet-on-py3' requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl + name: rich + version: 14.0.0 + sha256: 1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0 + requires_dist: + - ipywidgets>=7.5.1,<9 ; extra == 'jupyter' + - markdown-it-py>=2.2.0 + - pygments>=2.13.0,<3.0.0 + - typing-extensions>=4.0.0,<5.0 ; python_full_version < '3.11' + requires_python: '>=3.8.0' - pypi: https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl name: roman-numerals-py version: 3.1.0 @@ -1890,6 +2778,13 @@ packages: version: 0.25.1 sha256: a49e1d7a4978ed554f095430b89ecc23f42014a50ac385eb0c4d163ce213c325 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/04/9e/91fe10ea09e2383f0d0531cf3da765fa01b3b9918e85df92081813259cf4/ruamel.yaml-0.18.12-py3-none-any.whl name: ruamel-yaml version: 0.18.12 @@ -1979,11 +2874,21 @@ packages: requires_dist: - setuptools requires_python: '>=2.7' +- pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + name: shellingham + version: 1.5.4 + sha256: 7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686 + requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl name: six version: 1.17.0 sha256: 4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*' +- pypi: https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl + name: sniffio + version: 1.3.1 + sha256: 2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 + requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl name: snowballstemmer version: 2.2.0 @@ -1993,6 +2898,41 @@ packages: version: '2.7' sha256: 6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4 requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/8d/fd/42a1720542199ae6ff0f9c37bbd55dd3033ddd7bbe00d68cde09d6824887/sparse-0.17.0-py2.py3-none-any.whl + name: sparse + version: 0.17.0 + sha256: 1922d1d97f692b1061c4f03a1dd6ee21850aedc88e171aa845715f5069952f18 + requires_dist: + - numpy>=1.17 + - numba>=0.49 + - mkdocs-material ; extra == 'docs' + - mkdocstrings[python] ; extra == 'docs' + - mkdocs-gen-files ; extra == 'docs' + - mkdocs-literate-nav ; extra == 'docs' + - mkdocs-section-index ; extra == 'docs' + - mkdocs-jupyter ; extra == 'docs' + - sparse[extras] ; extra == 'docs' + - dask[array] ; extra == 'extras' + - sparse[finch] ; extra == 'extras' + - scipy ; extra == 'extras' + - scikit-learn ; extra == 'extras' + - networkx ; extra == 'extras' + - sparse[extras] ; extra == 'tests' + - pytest>=3.5 ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + - pre-commit ; extra == 'tests' + - pytest-codspeed ; extra == 'tests' + - sparse[tests] ; extra == 'tox' + - tox ; extra == 'tox' + - sparse[tests] ; extra == 'notebooks' + - nbmake ; extra == 'notebooks' + - matplotlib ; extra == 'notebooks' + - sparse[docs,mlir,notebooks,tox] ; extra == 'all' + - matrepr ; extra == 'all' + - finch-tensor>=0.2.12 ; extra == 'finch' + - finch-mlir>=0.0.2 ; extra == 'mlir' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl name: sphinx version: 8.2.3 @@ -2144,6 +3084,44 @@ packages: - sphinx>=5 ; extra == 'standalone' - pytest ; extra == 'test' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/5e/51/5ba9ea3246ea068630acf35a6ba0d181e99f1af1afd17e159eac7e8bc2b8/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: sqlalchemy + version: 2.0.41 + sha256: dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a + requires_dist: + - importlib-metadata ; python_full_version < '3.8' + - greenlet>=1 ; (python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and 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.3,!=0.2.4,!=0.2.6 ; 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/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl name: stack-data version: 0.6.3 @@ -2157,6 +3135,287 @@ packages: - pygments ; extra == 'tests' - littleutils ; extra == 'tests' - cython ; extra == 'tests' +- pypi: https://files.pythonhosted.org/packages/14/ba/d03f7ee711391af1d5f4dd7c44f8abdd06bce247028af2441ba8f6ff329b/stamina-25.1.0-py3-none-any.whl + name: stamina + version: 25.1.0 + sha256: c08291da540e6f4243c20f7ee98f0ed0ac9101d639803c481a029b56d7e9b45d + requires_dist: + - tenacity + - typing-extensions ; python_full_version < '3.10' + - anyio ; extra == 'dev' + - dirty-equals ; extra == 'dev' + - mypy>=1.4 ; extra == 'dev' + - nox>=2024.3.2 ; extra == 'dev' + - prometheus-client ; extra == 'dev' + - pytest ; extra == 'dev' + - structlog ; extra == 'dev' + - tomli ; python_full_version < '3.11' and extra == 'dev' + - trio ; extra == 'dev' + - uv ; extra == 'dev' + - furo ; extra == 'docs' + - myst-parser ; extra == 'docs' + - prometheus-client ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-notfound-page ; extra == 'docs' + - sphinx>=7.2.2 ; extra == 'docs' + - structlog ; extra == 'docs' + - anyio ; extra == 'tests' + - dirty-equals ; extra == 'tests' + - pytest ; extra == 'tests' + - mypy>=1.4 ; extra == 'typing' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl + name: starlette + version: 0.46.2 + sha256: 595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35 + requires_dist: + - anyio>=3.6.2,<5 + - typing-extensions>=3.10.0 ; python_full_version < '3.10' + - 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.9' +- pypi: https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl + name: tenacity + version: 9.1.2 + sha256: f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138 + requires_dist: + - reno ; extra == 'doc' + - sphinx ; extra == 'doc' + - pytest ; extra == 'test' + - tornado>=4.5 ; extra == 'test' + - typeguard ; extra == 'test' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/3a/d8/1ba8f32bfc9cb69e37edeca93738e883f478fbe84ae401f72c0d8d507841/tifffile-2025.6.11-py3-none-any.whl + name: tifffile + version: 2025.6.11 + sha256: 32effb78b10b3a283eb92d4ebf844ae7e93e151458b0412f38518b4e6d2d7542 + requires_dist: + - numpy + - imagecodecs>=2024.12.30 ; extra == 'codecs' + - defusedxml ; extra == 'xml' + - lxml ; extra == 'xml' + - zarr>=3 ; extra == 'zarr' + - fsspec ; extra == 'zarr' + - kerchunk ; extra == 'zarr' + - matplotlib ; extra == 'plot' + - imagecodecs>=2024.12.30 ; extra == 'all' + - matplotlib ; extra == 'all' + - defusedxml ; extra == 'all' + - lxml ; extra == 'all' + - zarr>=3 ; extra == 'all' + - fsspec ; extra == 'all' + - kerchunk ; extra == 'all' + - cmapfile ; extra == 'test' + - czifile ; extra == 'test' + - dask ; extra == 'test' + - defusedxml ; extra == 'test' + - fsspec ; extra == 'test' + - imagecodecs ; extra == 'test' + - kerchunk ; extra == 'test' + - lfdfiles ; extra == 'test' + - lxml ; extra == 'test' + - ndtiff ; extra == 'test' + - oiffile ; extra == 'test' + - psdtags ; extra == 'test' + - pytest ; extra == 'test' + - requests ; extra == 'test' + - roifile ; extra == 'test' + - xarray ; extra == 'test' + - zarr>=3 ; extra == 'test' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/37/63/383de8d9400a52d55683940387b160efb4f6b35bfb7645a443008780e2a9/tiled-0.1.0b29-py3-none-any.whl + name: tiled + version: 0.1.0b29 + sha256: 61623955309abed0fe3cfb834899a9f4a3e41e28e47f1f84fd98d88cddfa06d7 + requires_dist: + - httpx>=0.20.0,!=0.23.1 + - json-merge-patch + - jsonpatch + - jsonschema + - msgpack>=1.0.0 + - orjson + - platformdirs + - pydantic-settings>=2,<3 + - pydantic>=2,<3 + - pyyaml + - typer + - adbc-driver-manager ; extra == 'all' + - adbc-driver-postgresql ; extra == 'all' + - adbc-driver-sqlite ; extra == 'all' + - aiofiles ; extra == 'all' + - aiosqlite ; extra == 'all' + - alembic ; extra == 'all' + - anyio ; extra == 'all' + - asgi-correlation-id ; extra == 'all' + - asyncpg ; extra == 'all' + - awkward>=2.4.3 ; extra == 'all' + - blosc2 ; python_full_version >= '3.10' and extra == 'all' + - cachetools ; extra == 'all' + - canonicaljson ; extra == 'all' + - dask ; extra == 'all' + - dask[array] ; extra == 'all' + - dask[dataframe] ; extra == 'all' + - duckdb ; extra == 'all' + - entrypoints ; extra == 'all' + - fastapi>=0.115.8 ; extra == 'all' + - h5netcdf ; extra == 'all' + - h5py ; extra == 'all' + - hdf5plugin ; extra == 'all' + - jinja2 ; extra == 'all' + - jmespath ; extra == 'all' + - lz4 ; extra == 'all' + - ndindex ; extra == 'all' + - numcodecs<0.16.0 ; extra == 'all' + - numpy ; extra == 'all' + - openpyxl ; extra == 'all' + - packaging ; extra == 'all' + - pandas ; extra == 'all' + - pillow ; extra == 'all' + - prometheus-client ; extra == 'all' + - pyarrow ; extra == 'all' + - python-dateutil ; extra == 'all' + - python-jose[cryptography] ; extra == 'all' + - python-multipart ; extra == 'all' + - rich ; extra == 'all' + - sparse ; extra == 'all' + - sqlalchemy[asyncio]>=2 ; extra == 'all' + - stamina ; extra == 'all' + - starlette>=0.38.0 ; extra == 'all' + - tifffile ; extra == 'all' + - uvicorn[standard] ; extra == 'all' + - watchfiles ; extra == 'all' + - xarray ; extra == 'all' + - zarr<3 ; extra == 'all' + - zstandard ; extra == 'all' + - dask[array] ; extra == 'array' + - numpy ; extra == 'array' + - awkward>=2.4.3 ; extra == 'client' + - blosc2 ; python_full_version >= '3.10' and extra == 'client' + - dask[array] ; extra == 'client' + - dask[dataframe] ; extra == 'client' + - entrypoints ; extra == 'client' + - lz4 ; extra == 'client' + - ndindex ; extra == 'client' + - numpy ; extra == 'client' + - pandas ; extra == 'client' + - pyarrow ; extra == 'client' + - rich ; extra == 'client' + - sparse ; extra == 'client' + - stamina ; extra == 'client' + - watchfiles ; extra == 'client' + - xarray ; extra == 'client' + - zstandard ; extra == 'client' + - blosc2 ; python_full_version >= '3.10' and extra == 'compression' + - lz4 ; extra == 'compression' + - zstandard ; extra == 'compression' + - dask[dataframe] ; extra == 'dataframe' + - pandas ; extra == 'dataframe' + - pyarrow ; extra == 'dataframe' + - coverage ; extra == 'dev' + - flake8 ; extra == 'dev' + - importlib-resources ; python_full_version < '3.9' and extra == 'dev' + - ipython ; extra == 'dev' + - ldap3 ; extra == 'dev' + - matplotlib ; extra == 'dev' + - mistune ; extra == 'dev' + - myst-parser ; extra == 'dev' + - numpydoc ; extra == 'dev' + - pre-commit ; extra == 'dev' + - pytest-asyncio ; extra == 'dev' + - pytest-mock ; extra == 'dev' + - pytest-rerunfailures ; extra == 'dev' + - pytest<8 ; extra == 'dev' + - sphinx!=4.1.0,!=4.1.1,!=4.1.2,!=4.2.0 ; extra == 'dev' + - sphinx-click ; extra == 'dev' + - sphinx-copybutton ; extra == 'dev' + - sphinx-rtd-theme ; extra == 'dev' + - sphinxcontrib-mermaid ; extra == 'dev' + - h5netcdf ; extra == 'formats' + - h5py ; extra == 'formats' + - hdf5plugin ; extra == 'formats' + - openpyxl ; extra == 'formats' + - pillow ; extra == 'formats' + - tifffile ; extra == 'formats' + - entrypoints ; extra == 'minimal-client' + - rich ; extra == 'minimal-client' + - stamina ; extra == 'minimal-client' + - watchfiles ; extra == 'minimal-client' + - aiofiles ; extra == 'minimal-server' + - aiosqlite ; extra == 'minimal-server' + - alembic ; extra == 'minimal-server' + - anyio ; extra == 'minimal-server' + - asgi-correlation-id ; extra == 'minimal-server' + - cachetools ; extra == 'minimal-server' + - canonicaljson ; extra == 'minimal-server' + - dask ; extra == 'minimal-server' + - fastapi>=0.115.8 ; extra == 'minimal-server' + - jinja2 ; extra == 'minimal-server' + - jmespath ; extra == 'minimal-server' + - numcodecs<0.16.0 ; extra == 'minimal-server' + - packaging ; extra == 'minimal-server' + - prometheus-client ; extra == 'minimal-server' + - python-dateutil ; extra == 'minimal-server' + - python-jose[cryptography] ; extra == 'minimal-server' + - python-multipart ; extra == 'minimal-server' + - sqlalchemy[asyncio]>=2 ; extra == 'minimal-server' + - starlette>=0.38.0 ; extra == 'minimal-server' + - uvicorn[standard] ; extra == 'minimal-server' + - zarr<3 ; extra == 'minimal-server' + - adbc-driver-manager ; extra == 'server' + - adbc-driver-postgresql ; extra == 'server' + - adbc-driver-sqlite ; extra == 'server' + - aiofiles ; extra == 'server' + - aiosqlite ; extra == 'server' + - alembic ; extra == 'server' + - anyio ; extra == 'server' + - asgi-correlation-id ; extra == 'server' + - asyncpg ; extra == 'server' + - awkward>=2.4.3 ; extra == 'server' + - blosc2 ; python_full_version >= '3.10' and extra == 'server' + - cachetools ; extra == 'server' + - canonicaljson ; extra == 'server' + - dask ; extra == 'server' + - dask[array] ; extra == 'server' + - dask[dataframe] ; extra == 'server' + - duckdb ; extra == 'server' + - fastapi>=0.115.8 ; extra == 'server' + - h5netcdf ; extra == 'server' + - h5py ; extra == 'server' + - hdf5plugin ; extra == 'server' + - jinja2 ; extra == 'server' + - jmespath ; extra == 'server' + - lz4 ; extra == 'server' + - ndindex ; extra == 'server' + - numcodecs<0.16.0 ; extra == 'server' + - numpy ; extra == 'server' + - openpyxl ; extra == 'server' + - packaging ; extra == 'server' + - pandas ; extra == 'server' + - pillow ; extra == 'server' + - prometheus-client ; extra == 'server' + - pyarrow ; extra == 'server' + - python-dateutil ; extra == 'server' + - python-jose[cryptography] ; extra == 'server' + - python-multipart ; extra == 'server' + - sparse ; extra == 'server' + - sqlalchemy[asyncio]>=2 ; extra == 'server' + - starlette>=0.38.0 ; extra == 'server' + - tifffile ; extra == 'server' + - uvicorn[standard] ; extra == 'server' + - xarray ; extra == 'server' + - zarr<3 ; extra == 'server' + - zstandard ; extra == 'server' + - ndindex ; extra == 'sparse' + - pyarrow ; extra == 'sparse' + - sparse ; extra == 'sparse' + - dask[array] ; extra == 'xarray' + - pandas ; extra == 'xarray' + - pyarrow ; extra == 'xarray' + - xarray ; extra == 'xarray' + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda sha256: e0569c9caa68bf476bead1bed3d79650bb080b532c64a4af7d8ca286c08dea4e md5: d453b98d9c83e71da0741bb0ff4d76bc @@ -2204,6 +3463,16 @@ packages: - pytest-mypy-testing ; extra == 'test' - pytest>=7.0,<8.2 ; extra == 'test' requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl + name: typer + version: 0.16.0 + sha256: 1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855 + requires_dist: + - click>=8.0.0 + - typing-extensions>=3.7.4.3 + - shellingham>=1.3.0 + - rich>=10.11.0 + requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl name: typing-extensions version: 4.13.2 @@ -2239,6 +3508,39 @@ packages: - pysocks>=1.5.6,!=1.5.7,<2.0 ; extra == 'socks' - zstandard>=0.18.0 ; extra == 'zstd' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl + name: uvicorn + version: 0.35.0 + sha256: 197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a + requires_dist: + - click>=7.0 + - h11>=0.8 + - typing-extensions>=4.0 ; python_full_version < '3.11' + - colorama>=0.4 ; sys_platform == 'win32' and extra == 'standard' + - httptools>=0.6.3 ; 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.13 ; extra == 'standard' + - websockets>=10.4 ; extra == 'standard' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: uvloop + version: 0.21.0 + sha256: f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816 + requires_dist: + - setuptools>=60 ; extra == 'dev' + - cython~=3.0 ; extra == 'dev' + - sphinx~=4.1.2 ; extra == 'docs' + - sphinxcontrib-asyncio~=0.3.0 ; extra == 'docs' + - sphinx-rtd-theme~=0.5.2 ; extra == 'docs' + - aiohttp>=3.10.5 ; extra == 'test' + - flake8~=5.0 ; extra == 'test' + - psutil ; extra == 'test' + - pycodestyle~=2.9.0 ; extra == 'test' + - pyopenssl~=23.0.0 ; extra == 'test' + - mypy>=0.800 ; extra == 'test' + requires_python: '>=3.8.0' - pypi: https://files.pythonhosted.org/packages/4c/ed/3cfeb48175f0671ec430ede81f628f9fb2b1084c9064ca67ebe8c0ed6a05/virtualenv-20.30.0-py3-none-any.whl name: virtualenv version: 20.30.0 @@ -2268,17 +3570,97 @@ packages: - setuptools>=68 ; extra == 'test' - time-machine>=2.10 ; platform_python_implementation == 'CPython' and extra == 'test' requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/25/0d/4d564798a49bf5482a4fa9416dea6b6c0733a3b5700cb8a5a503c4b15853/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: watchfiles + version: 1.1.0 + sha256: d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c + requires_dist: + - anyio>=3.0.0 + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl name: wcwidth version: 0.2.13 sha256: 3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859 requires_dist: - backports-functools-lru-cache>=1.2.1 ; python_full_version < '3.2' +- pypi: https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: websockets + version: 15.0.1 + sha256: 0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8 + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl name: wrapt version: 1.17.2 sha256: 8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/82/8a/6b50c1dd2260d407c1a499d47cf829f59f07007e0dcebafdabb24d1d26a5/xarray-2025.6.1-py3-none-any.whl + name: xarray + version: 2025.6.1 + sha256: 8b988b47f67a383bdc3b04c5db475cd165e580134c1f1943d52aee4a9c97651b + requires_dist: + - numpy>=1.24 + - packaging>=23.2 + - pandas>=2.1 + - scipy ; extra == 'accel' + - bottleneck ; extra == 'accel' + - numbagg ; extra == 'accel' + - numba>=0.54 ; extra == 'accel' + - flox ; extra == 'accel' + - opt-einsum ; extra == 'accel' + - xarray[accel,etc,io,parallel,viz] ; extra == 'complete' + - netcdf4 ; extra == 'io' + - h5netcdf ; extra == 'io' + - scipy ; extra == 'io' + - pydap ; python_full_version < '3.10' and extra == 'io' + - zarr ; extra == 'io' + - fsspec ; extra == 'io' + - cftime ; extra == 'io' + - pooch ; extra == 'io' + - sparse ; extra == 'etc' + - dask[complete] ; extra == 'parallel' + - cartopy ; extra == 'viz' + - matplotlib ; extra == 'viz' + - nc-time-axis ; extra == 'viz' + - seaborn ; extra == 'viz' + - pandas-stubs ; extra == 'types' + - scipy-stubs ; extra == 'types' + - types-pyyaml ; extra == 'types' + - types-pygments ; extra == 'types' + - types-colorama ; extra == 'types' + - types-decorator ; extra == 'types' + - types-defusedxml ; extra == 'types' + - types-docutils ; extra == 'types' + - types-networkx ; extra == 'types' + - types-pexpect ; extra == 'types' + - types-psutil ; extra == 'types' + - types-pycurl ; extra == 'types' + - types-openpyxl ; extra == 'types' + - types-python-dateutil ; extra == 'types' + - types-pytz ; extra == 'types' + - types-setuptools ; extra == 'types' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/5e/d8/9ffd8c237b3559945bb52103cf0eed64ea098f7b7f573f8d2962ef27b4b2/zarr-2.18.7-py3-none-any.whl + name: zarr + version: 2.18.7 + sha256: ac3dc4033e9ae4e9d7b5e27c97ea3eaf1003cc0a07f010bd83d5134bf8c4b223 + requires_dist: + - asciitree + - numpy>=1.24 + - fasteners ; sys_platform != 'emscripten' + - numcodecs>=0.10.0,!=0.14.0,!=0.14.1,<0.16 + - notebook ; extra == 'jupyter' + - ipytree>=0.2.2 ; extra == 'jupyter' + - ipywidgets>=8.0.0 ; extra == 'jupyter' + - sphinx ; extra == 'docs' + - sphinx-automodapi ; extra == 'docs' + - sphinx-design ; extra == 'docs' + - sphinx-issues ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - pydata-sphinx-theme ; extra == 'docs' + - numpydoc ; extra == 'docs' + - numcodecs[msgpack]!=0.14.0,!=0.14.1,<0.16 ; extra == 'docs' + - pytest-doctestplus ; extra == 'docs' + requires_python: '>=3.11' - pypi: https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl name: zipp version: 3.21.0 @@ -2304,3 +3686,11 @@ packages: - importlib-resources ; python_full_version < '3.9' and extra == 'test' - pytest-mypy ; extra == 'type' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/fa/18/89ac62eac46b69948bf35fcd90d37103f38722968e2981f752d69081ec4d/zstandard-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: zstandard + version: 0.23.0 + sha256: 6f77fa49079891a4aab203d0b1744acc85577ed16d767b52fc089d83faf8d8ed + requires_dist: + - cffi>=1.17 ; platform_python_implementation == 'PyPy' + - cffi>=1.17 ; extra == 'cffi' + requires_python: '>=3.8' diff --git a/pyproject.toml b/pyproject.toml index a075df7a..03a13fa6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,10 +37,13 @@ dependencies = [ [project.optional-dependencies] test = [ + "entrypoints", "pytest >=6", "pytest-cov >=3", "pytest-asyncio >=0.18", "caproto[standard] >=0.4.2rc1,!=1.2.0", + "tiled[minimal-client]", + "tiled[minimal-server]", ] dev = [ "caproto[standard] >=0.4.2rc1,!=1.2.0", @@ -52,6 +55,7 @@ dev = [ "ipython", "ruff", "aioca", + "tiled[minimal-client,server]", ] docs = [ "sphinx>=7.0", @@ -73,13 +77,11 @@ black-hole-ioc = "cditools.simulated.black_hole:main" [tool.setuptools_scm] write_to = "src/cditools/_version.py" - [tool.uv] dev-dependencies = [ "cditools[test]", ] - [tool.pytest.ini_options] minversion = "6.0" addopts = ["-ra", "--showlocals", "--strict-markers", "--strict-config"] @@ -154,7 +156,6 @@ isort.required-imports = ["from __future__ import annotations"] "tests/**" = ["T20", "ARG001"] "noxfile.py" = ["T20"] - [tool.pylint] py-version = "3.9" ignore-paths = [".*/_version.py"] diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index d83991c4..59fac427 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -15,6 +15,7 @@ from bluesky.protocols import StreamAsset from event_model import DataKey from ophyd_async.core import ( + AsyncStatus, DatasetDescriber, DetectorTrigger, HDFDatasetDescription, @@ -334,14 +335,14 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK data_key=f"{name}_y_pixel_size", dataset="entry/instrument/detector/y_pixel_size", shape=(exposures_per_event,), - dtype_numpy=np.dtype(np.float32).str, + dtype_numpy=np.dtype(np.uint8).str, chunk_shape=(1,), ), HDFDatasetDescription( data_key=f"{name}_x_pixel_size", dataset="entry/instrument/detector/x_pixel_size", shape=(exposures_per_event,), - dtype_numpy=np.dtype(np.float32).str, + dtype_numpy=np.dtype(np.uint8).str, chunk_shape=(1,), ), HDFDatasetDescription( @@ -369,14 +370,14 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK data_key=f"{name}_beam_center_x", dataset="entry/instrument/detector/beam_center_x", shape=(exposures_per_event,), - dtype_numpy=np.dtype(np.float32).str, + dtype_numpy=np.dtype(np.uint8).str, chunk_shape=(1,), ), HDFDatasetDescription( data_key=f"{name}_beam_center_y", dataset="entry/instrument/detector/beam_center_y", shape=(exposures_per_event,), - dtype_numpy=np.dtype(np.float32).str, + dtype_numpy=np.dtype(np.uint8).str, chunk_shape=(1,), ), HDFDatasetDescription( @@ -437,6 +438,7 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK self._datasets = master_datasets + frame_datasets + logger.warning("OPENING FILE") return { ds.data_key: DataKey( source=self._master_file_path.as_posix(), @@ -453,6 +455,7 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK @property def _master_file_path(self) -> Path | None: if self._current_sequence_id is None or self._file_info is None: + logger.warning("No master file path found for sequence id %s and file info %s", self._current_sequence_id, self._file_info) return None return ( self._file_info.directory_path @@ -479,6 +482,7 @@ async def collect_stream_docs( async def close(self) -> None: """Clean up file writing after acquisition and validate files exist.""" # Disable file writer and reset number of images per file + logger.warning("CLOSING FILE") await asyncio.gather( self.fileio.fw_enable.set(False), self.fileio.save_files.set(False), @@ -572,6 +576,7 @@ def __init__( # TODO: Fix typing in base class self.driver: EigerDriverIO = cast(EigerDriverIO, self.driver) + @AsyncStatus.wrap async def stage(self) -> None: """Prepare the eiger detector for acquisition. diff --git a/tests/conftest.py b/tests/conftest.py index 2d58239b..ba353283 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,11 @@ import asyncio import contextlib import pprint +from collections.abc import Generator + +from tiled.client import from_uri +from tiled.client.container import Container +from tiled.server.simple import SimpleTiledServer import pytest from bluesky.run_engine import RunEngine, TransitionError @@ -10,6 +15,9 @@ _ALLOWED_PYTEST_TASKS = {"async_finalizer", "async_setup", "async_teardown"} +# ================================================================================== +# Copied from ophyd-async conftest.py +# ================================================================================== def _error_and_kill_pending_tasks( loop: asyncio.AbstractEventLoop, test_name: str, test_passed: bool ) -> set[asyncio.Task]: @@ -75,3 +83,12 @@ def clean_event_loop(): yield RE clean_event_loop() +# ================================================================================== + +@pytest.fixture +def tiled_client() -> Generator[Container, None, None]: + server: SimpleTiledServer = SimpleTiledServer() + client: Container = from_uri(server.uri) + yield client + server.close() + diff --git a/tests/test_eiger_async.py b/tests/test_eiger_async.py index 6cd97f17..75c44ff4 100644 --- a/tests/test_eiger_async.py +++ b/tests/test_eiger_async.py @@ -5,10 +5,17 @@ from __future__ import annotations import asyncio +from collections.abc import Sequence +import os +import shutil +import h5py import numpy as np import pytest from bluesky.run_engine import RunEngine +from bluesky.callbacks.tiled_writer import TiledWriter +import bluesky.plan_stubs as bps +from bluesky.utils import MsgGenerator from ophyd_async.core import ( DetectorTrigger, PathProvider, @@ -22,6 +29,9 @@ set_mock_value, ) +from tiled.server.simple import SimpleTiledServer +from tiled.client.container import Container + from cditools.eiger_async import ( EigerController, EigerDataSource, @@ -34,6 +44,25 @@ ) +def write_eiger_hdf5_file(num_triggers: int, num_images: int, sequence_id: int, name: str = "test_eiger"): + if not os.path.exists(f"/tmp/test_data/"): + os.makedirs(f"/tmp/test_data/") + + with h5py.File(f"/tmp/test_data/{name}_{sequence_id}_data_000001.h5", "w") as f: + f.create_dataset("data_000001", data=np.zeros((num_triggers * num_images, 2048, 2048), dtype=np.uint16)) + + with h5py.File(f"/tmp/test_data/{name}_{sequence_id}_master.h5", "w") as f: + f["entry/data/data_000001"] = h5py.ExternalLink(f"/tmp/test_data/{name}_{sequence_id}_data_000001.h5", "data_000001") + f.create_dataset("entry/instrument/detector/y_pixel_size", data=np.ones((num_images,), dtype=np.uint8)) + f.create_dataset("entry/instrument/detector/x_pixel_size", data=np.ones((num_images,), dtype=np.uint8)) + f.create_dataset("entry/instrument/detector/distance", data=np.ones((num_images,), dtype=np.float32)) + f.create_dataset("entry/instrument/detector/incident_wavelength", data=np.ones((num_images,), dtype=np.float32)) + f.create_dataset("entry/instrument/detector/frame_time", data=np.ones((num_images,), dtype=np.float32)) + f.create_dataset("entry/instrument/detector/beam_center_x", data=np.ones((num_images,), dtype=np.uint8)) + f.create_dataset("entry/instrument/detector/beam_center_y", data=np.ones((num_images,), dtype=np.uint8)) + f.create_dataset("entry/instrument/detector/count_time", data=np.ones((num_images,), dtype=np.float32)) + f.create_dataset("entry/instrument/detector/pixel_mask", data=np.zeros((num_images, 2048, 2048), dtype=np.uint8)) + @pytest.fixture def mock_eiger_detector(RE: RunEngine) -> EigerDetector: path_provider = StaticPathProvider( @@ -44,7 +73,11 @@ def mock_eiger_detector(RE: RunEngine) -> EigerDetector: set_mock_value(detector.driver.array_size_x, 2048) set_mock_value(detector.driver.array_size_y, 2048) set_mock_value(detector.driver.data_type, "UInt16") - return detector + + yield detector + + if os.path.exists("/tmp/test_data"): + shutil.rmtree("/tmp/test_data") @pytest.fixture @@ -513,3 +546,95 @@ async def _simulate_one_trigger(num_captured: int): ) await mock_eiger_detector.kickoff() await mock_eiger_detector.complete() + + +@pytest.mark.asyncio +async def test_eiger_detector_with_RE(RE: RunEngine, tiled_client: Container, mock_eiger_detector: EigerDetector) -> None: + set_mock_value(mock_eiger_detector.fileio.sequence_id, 1) + set_mock_value(mock_eiger_detector.driver.num_images, 1) + set_mock_value(mock_eiger_detector.driver.num_triggers, 1) + acquire_period = 0.01 + + def _count_plan(dets: Sequence[EigerDetector], num: int = 1, num_images: int = 1, sequence_id: int = 1) -> MsgGenerator[str]: + yield from bps.stage_all(*dets) + yield from bps.open_run() + + for _ in range(num): + read_values = {} + for det in dets: + read_values[det] = yield from bps.rd(det.fileio.num_captured) + + for det in dets: + yield from bps.trigger(det, wait=False, group="wait_for_trigger") + + yield from bps.sleep(acquire_period) + + write_eiger_hdf5_file(num_triggers=1, num_images=num_images, sequence_id=sequence_id, name="test_eiger") + + for det in dets: + num_images = yield from bps.rd(det.driver.num_images) + set_mock_value(det.fileio.num_captured, read_values[det] + num_images) + + yield from bps.wait(group="wait_for_trigger") + yield from bps.create() + + for det in dets: + yield from bps.read(det) + + yield from bps.save() + + yield from bps.close_run() + yield from bps.unstage_all(*dets) + + tiled_writer = TiledWriter(tiled_client) + RE.subscribe(tiled_writer) + + uid = RE(_count_plan([mock_eiger_detector])) + assert uid is not None + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_1"].shape == (1, 2048, 2048) + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_1"].dtype == np.uint16 + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_x_pixel_size"].shape == (1,) + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_x_pixel_size"].dtype == np.uint8 + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_y_pixel_size"].shape == (1,) + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_y_pixel_size"].dtype == np.uint8 + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_detector_distance"].shape == (1,) + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_detector_distance"].dtype == np.float32 + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_incident_wavelength"].shape == (1,) + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_incident_wavelength"].dtype == np.float32 + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_frame_time"].shape == (1,) + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_frame_time"].dtype == np.float32 + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_beam_center_x"].shape == (1,) + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_beam_center_x"].dtype == np.uint8 + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_beam_center_y"].shape == (1,) + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_beam_center_y"].dtype == np.uint8 + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_count_time"].shape == (1,) + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_count_time"].dtype == np.float32 + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_pixel_mask"].shape == (1, 2048, 2048) + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_pixel_mask"].dtype == np.uint8 + + set_mock_value(mock_eiger_detector.fileio.sequence_id, 2) + set_mock_value(mock_eiger_detector.driver.num_images, 5) + + + uid = RE(_count_plan([mock_eiger_detector], num=10, num_images=5, sequence_id=2)) + assert uid is not None + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_1"].shape == (50, 2048, 2048) + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_1"].dtype == np.uint16 + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_x_pixel_size"].shape == (50,) + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_x_pixel_size"].dtype == np.uint8 + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_y_pixel_size"].shape == (50,) + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_y_pixel_size"].dtype == np.uint8 + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_detector_distance"].shape == (50,) + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_detector_distance"].dtype == np.float32 + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_incident_wavelength"].shape == (50,) + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_incident_wavelength"].dtype == np.float32 + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_frame_time"].shape == (50,) + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_frame_time"].dtype == np.float32 + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_beam_center_x"].shape == (50,) + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_beam_center_x"].dtype == np.uint8 + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_beam_center_y"].shape == (50,) + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_beam_center_y"].dtype == np.uint8 + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_count_time"].shape == (50,) + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_count_time"].dtype == np.float32 + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_pixel_mask"].shape == (50, 2048, 2048) + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_pixel_mask"].dtype == np.uint8 \ No newline at end of file From 08451d8df3ab154fa71edd42e9b66c3594a3fd96 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Thu, 10 Jul 2025 15:26:50 -0400 Subject: [PATCH 14/46] RunEngine test passes --- src/cditools/eiger_async.py | 1 + tests/test_eiger_async.py | 38 +++++++++++++++++++------------------ 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index 59fac427..1cca924f 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -467,6 +467,7 @@ async def collect_stream_docs( ) -> AsyncIterator[StreamAsset]: """Generate stream documents for the written HDF5 files.""" if indices_written: + print(f"{indices_written=}") if not self._composer: self._composer = HDFDocumentComposer( self._master_file_path, diff --git a/tests/test_eiger_async.py b/tests/test_eiger_async.py index 75c44ff4..2471e5c9 100644 --- a/tests/test_eiger_async.py +++ b/tests/test_eiger_async.py @@ -550,13 +550,14 @@ async def _simulate_one_trigger(num_captured: int): @pytest.mark.asyncio async def test_eiger_detector_with_RE(RE: RunEngine, tiled_client: Container, mock_eiger_detector: EigerDetector) -> None: - set_mock_value(mock_eiger_detector.fileio.sequence_id, 1) - set_mock_value(mock_eiger_detector.driver.num_images, 1) - set_mock_value(mock_eiger_detector.driver.num_triggers, 1) acquire_period = 0.01 def _count_plan(dets: Sequence[EigerDetector], num: int = 1, num_images: int = 1, sequence_id: int = 1) -> MsgGenerator[str]: yield from bps.stage_all(*dets) + + for det in dets: + set_mock_value(det.fileio.num_captured, 0) + yield from bps.open_run() for _ in range(num): @@ -589,6 +590,9 @@ def _count_plan(dets: Sequence[EigerDetector], num: int = 1, num_images: int = 1 tiled_writer = TiledWriter(tiled_client) RE.subscribe(tiled_writer) + set_mock_value(mock_eiger_detector.fileio.sequence_id, 1) + set_mock_value(mock_eiger_detector.driver.num_images, 1) + set_mock_value(mock_eiger_detector.driver.num_triggers, 1) uid = RE(_count_plan([mock_eiger_detector])) assert uid is not None assert tiled_client.values().last()["streams"]["primary"]["test_eiger_1"].shape == (1, 2048, 2048) @@ -613,28 +617,26 @@ def _count_plan(dets: Sequence[EigerDetector], num: int = 1, num_images: int = 1 assert tiled_client.values().last()["streams"]["primary"]["test_eiger_pixel_mask"].dtype == np.uint8 set_mock_value(mock_eiger_detector.fileio.sequence_id, 2) - set_mock_value(mock_eiger_detector.driver.num_images, 5) - - - uid = RE(_count_plan([mock_eiger_detector], num=10, num_images=5, sequence_id=2)) + set_mock_value(mock_eiger_detector.driver.num_images, 1) + uid = RE(_count_plan([mock_eiger_detector], num=10, num_images=1, sequence_id=2)) assert uid is not None - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_1"].shape == (50, 2048, 2048) + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_1"].shape == (10, 2048, 2048) assert tiled_client.values().last()["streams"]["primary"]["test_eiger_1"].dtype == np.uint16 - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_x_pixel_size"].shape == (50,) + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_x_pixel_size"].shape == (10,) assert tiled_client.values().last()["streams"]["primary"]["test_eiger_x_pixel_size"].dtype == np.uint8 - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_y_pixel_size"].shape == (50,) + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_y_pixel_size"].shape == (10,) assert tiled_client.values().last()["streams"]["primary"]["test_eiger_y_pixel_size"].dtype == np.uint8 - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_detector_distance"].shape == (50,) + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_detector_distance"].shape == (10,) assert tiled_client.values().last()["streams"]["primary"]["test_eiger_detector_distance"].dtype == np.float32 - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_incident_wavelength"].shape == (50,) + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_incident_wavelength"].shape == (10,) assert tiled_client.values().last()["streams"]["primary"]["test_eiger_incident_wavelength"].dtype == np.float32 - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_frame_time"].shape == (50,) + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_frame_time"].shape == (10,) assert tiled_client.values().last()["streams"]["primary"]["test_eiger_frame_time"].dtype == np.float32 - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_beam_center_x"].shape == (50,) + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_beam_center_x"].shape == (10,) assert tiled_client.values().last()["streams"]["primary"]["test_eiger_beam_center_x"].dtype == np.uint8 - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_beam_center_y"].shape == (50,) + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_beam_center_y"].shape == (10,) assert tiled_client.values().last()["streams"]["primary"]["test_eiger_beam_center_y"].dtype == np.uint8 - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_count_time"].shape == (50,) + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_count_time"].shape == (10,) assert tiled_client.values().last()["streams"]["primary"]["test_eiger_count_time"].dtype == np.float32 - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_pixel_mask"].shape == (50, 2048, 2048) - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_pixel_mask"].dtype == np.uint8 \ No newline at end of file + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_pixel_mask"].shape == (10, 2048, 2048) + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_pixel_mask"].dtype == np.uint8 From d61a0eede6187c71dc06780f0e597c1b4aee33eb Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Thu, 10 Jul 2025 16:40:12 -0400 Subject: [PATCH 15/46] Some pre-commit stuff --- pixi.lock | 4 +- pyproject.toml | 2 +- src/cditools/eiger_async.py | 11 +- tests/conftest.py | 9 +- tests/test_eiger_async.py | 323 ++++++++++++++++++++++++++++-------- 5 files changed, 269 insertions(+), 80 deletions(-) diff --git a/pixi.lock b/pixi.lock index 15f2fb0f..e69a24bf 100644 --- a/pixi.lock +++ b/pixi.lock @@ -996,8 +996,8 @@ packages: requires_python: '>=3.8' - pypi: . name: cditools - version: 0.1.dev35+gd6e4bc7.d20250702 - sha256: a9151703cfcc98e3ec8b42f6f48a3d0b846f2cbe302adfad988cb1c3e94ab350 + version: 0.1.dev49+ga136df1.d20250710 + sha256: e49a6d9c33ef6c8f529bf8e6538f503155c3fa5dab0cf30261a0f1fb99086aaf requires_dist: - ophyd - ophyd-async>=0.10.0a4 diff --git a/pyproject.toml b/pyproject.toml index 03a13fa6..d657c6f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,7 +106,7 @@ report.exclude_also = [ files = ["src", "tests"] python_version = "3.9" warn_unused_configs = true -strict = true +strict = true enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] warn_unreachable = true disallow_untyped_defs = false diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index 1cca924f..251ed1eb 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -11,9 +11,9 @@ from typing import Annotated as A from typing import Any, cast -import numpy as np +import numpy as np # type: ignore[import-not-found] from bluesky.protocols import StreamAsset -from event_model import DataKey +from event_model import DataKey # type: ignore[import-untyped] from ophyd_async.core import ( AsyncStatus, DatasetDescriber, @@ -455,7 +455,11 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK @property def _master_file_path(self) -> Path | None: if self._current_sequence_id is None or self._file_info is None: - logger.warning("No master file path found for sequence id %s and file info %s", self._current_sequence_id, self._file_info) + logger.warning( + "No master file path found for sequence id %s and file info %s", + self._current_sequence_id, + self._file_info, + ) return None return ( self._file_info.directory_path @@ -467,7 +471,6 @@ async def collect_stream_docs( ) -> AsyncIterator[StreamAsset]: """Generate stream documents for the written HDF5 files.""" if indices_written: - print(f"{indices_written=}") if not self._composer: self._composer = HDFDocumentComposer( self._master_file_path, diff --git a/tests/conftest.py b/tests/conftest.py index ba353283..57953f31 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,13 +5,12 @@ import pprint from collections.abc import Generator +import pytest +from bluesky.run_engine import RunEngine, TransitionError from tiled.client import from_uri from tiled.client.container import Container from tiled.server.simple import SimpleTiledServer -import pytest -from bluesky.run_engine import RunEngine, TransitionError - _ALLOWED_PYTEST_TASKS = {"async_finalizer", "async_setup", "async_teardown"} @@ -83,12 +82,14 @@ def clean_event_loop(): yield RE clean_event_loop() + + # ================================================================================== + @pytest.fixture def tiled_client() -> Generator[Container, None, None]: server: SimpleTiledServer = SimpleTiledServer() client: Container = from_uri(server.uri) yield client server.close() - diff --git a/tests/test_eiger_async.py b/tests/test_eiger_async.py index 2471e5c9..aba76e66 100644 --- a/tests/test_eiger_async.py +++ b/tests/test_eiger_async.py @@ -5,16 +5,16 @@ from __future__ import annotations import asyncio -from collections.abc import Sequence -import os import shutil +from collections.abc import Sequence +from pathlib import Path +import bluesky.plan_stubs as bps import h5py import numpy as np import pytest -from bluesky.run_engine import RunEngine from bluesky.callbacks.tiled_writer import TiledWriter -import bluesky.plan_stubs as bps +from bluesky.run_engine import RunEngine from bluesky.utils import MsgGenerator from ophyd_async.core import ( DetectorTrigger, @@ -28,8 +28,6 @@ from ophyd_async.testing import ( set_mock_value, ) - -from tiled.server.simple import SimpleTiledServer from tiled.client.container import Container from cditools.eiger_async import ( @@ -44,24 +42,60 @@ ) -def write_eiger_hdf5_file(num_triggers: int, num_images: int, sequence_id: int, name: str = "test_eiger"): - if not os.path.exists(f"/tmp/test_data/"): - os.makedirs(f"/tmp/test_data/") +def write_eiger_hdf5_file( + num_triggers: int, num_images: int, sequence_id: int, name: str = "test_eiger" +): + data_path = Path("/tmp/test_data/") + if not data_path.exists(): + data_path.mkdir(parents=True) with h5py.File(f"/tmp/test_data/{name}_{sequence_id}_data_000001.h5", "w") as f: - f.create_dataset("data_000001", data=np.zeros((num_triggers * num_images, 2048, 2048), dtype=np.uint16)) + f.create_dataset( + "data_000001", + data=np.zeros((num_triggers * num_images, 2048, 2048), dtype=np.uint16), + ) with h5py.File(f"/tmp/test_data/{name}_{sequence_id}_master.h5", "w") as f: - f["entry/data/data_000001"] = h5py.ExternalLink(f"/tmp/test_data/{name}_{sequence_id}_data_000001.h5", "data_000001") - f.create_dataset("entry/instrument/detector/y_pixel_size", data=np.ones((num_images,), dtype=np.uint8)) - f.create_dataset("entry/instrument/detector/x_pixel_size", data=np.ones((num_images,), dtype=np.uint8)) - f.create_dataset("entry/instrument/detector/distance", data=np.ones((num_images,), dtype=np.float32)) - f.create_dataset("entry/instrument/detector/incident_wavelength", data=np.ones((num_images,), dtype=np.float32)) - f.create_dataset("entry/instrument/detector/frame_time", data=np.ones((num_images,), dtype=np.float32)) - f.create_dataset("entry/instrument/detector/beam_center_x", data=np.ones((num_images,), dtype=np.uint8)) - f.create_dataset("entry/instrument/detector/beam_center_y", data=np.ones((num_images,), dtype=np.uint8)) - f.create_dataset("entry/instrument/detector/count_time", data=np.ones((num_images,), dtype=np.float32)) - f.create_dataset("entry/instrument/detector/pixel_mask", data=np.zeros((num_images, 2048, 2048), dtype=np.uint8)) + f["entry/data/data_000001"] = h5py.ExternalLink( + f"/tmp/test_data/{name}_{sequence_id}_data_000001.h5", "data_000001" + ) + f.create_dataset( + "entry/instrument/detector/y_pixel_size", + data=np.ones((num_images,), dtype=np.uint8), + ) + f.create_dataset( + "entry/instrument/detector/x_pixel_size", + data=np.ones((num_images,), dtype=np.uint8), + ) + f.create_dataset( + "entry/instrument/detector/distance", + data=np.ones((num_images,), dtype=np.float32), + ) + f.create_dataset( + "entry/instrument/detector/incident_wavelength", + data=np.ones((num_images,), dtype=np.float32), + ) + f.create_dataset( + "entry/instrument/detector/frame_time", + data=np.ones((num_images,), dtype=np.float32), + ) + f.create_dataset( + "entry/instrument/detector/beam_center_x", + data=np.ones((num_images,), dtype=np.uint8), + ) + f.create_dataset( + "entry/instrument/detector/beam_center_y", + data=np.ones((num_images,), dtype=np.uint8), + ) + f.create_dataset( + "entry/instrument/detector/count_time", + data=np.ones((num_images,), dtype=np.float32), + ) + f.create_dataset( + "entry/instrument/detector/pixel_mask", + data=np.zeros((num_images, 2048, 2048), dtype=np.uint8), + ) + @pytest.fixture def mock_eiger_detector(RE: RunEngine) -> EigerDetector: @@ -76,8 +110,9 @@ def mock_eiger_detector(RE: RunEngine) -> EigerDetector: yield detector - if os.path.exists("/tmp/test_data"): - shutil.rmtree("/tmp/test_data") + data_path = Path("/tmp/test_data/") + if data_path.exists(): + shutil.rmtree(data_path) @pytest.fixture @@ -97,9 +132,7 @@ def mock_eiger_driver(RE: RunEngine) -> EigerDriverIO: @pytest.fixture def mock_eiger_fileio(RE: RunEngine) -> EigerFileIO: with init_devices(mock=True): - fileio = EigerFileIO("MOCK:EIGER:cam1:") - - return fileio + return EigerFileIO("MOCK:EIGER:cam1:") @pytest.fixture @@ -549,10 +582,17 @@ async def _simulate_one_trigger(num_captured: int): @pytest.mark.asyncio -async def test_eiger_detector_with_RE(RE: RunEngine, tiled_client: Container, mock_eiger_detector: EigerDetector) -> None: +async def test_eiger_detector_with_RE( + RE: RunEngine, tiled_client: Container, mock_eiger_detector: EigerDetector +) -> None: acquire_period = 0.01 - def _count_plan(dets: Sequence[EigerDetector], num: int = 1, num_images: int = 1, sequence_id: int = 1) -> MsgGenerator[str]: + def _count_plan( + dets: Sequence[EigerDetector], + num: int = 1, + num_images: int = 1, + sequence_id: int = 1, + ) -> MsgGenerator[str]: yield from bps.stage_all(*dets) for det in dets: @@ -564,13 +604,18 @@ def _count_plan(dets: Sequence[EigerDetector], num: int = 1, num_images: int = 1 read_values = {} for det in dets: read_values[det] = yield from bps.rd(det.fileio.num_captured) - + for det in dets: yield from bps.trigger(det, wait=False, group="wait_for_trigger") yield from bps.sleep(acquire_period) - write_eiger_hdf5_file(num_triggers=1, num_images=num_images, sequence_id=sequence_id, name="test_eiger") + write_eiger_hdf5_file( + num_triggers=1, + num_images=num_images, + sequence_id=sequence_id, + name="test_eiger", + ) for det in dets: num_images = yield from bps.rd(det.driver.num_images) @@ -595,48 +640,188 @@ def _count_plan(dets: Sequence[EigerDetector], num: int = 1, num_images: int = 1 set_mock_value(mock_eiger_detector.driver.num_triggers, 1) uid = RE(_count_plan([mock_eiger_detector])) assert uid is not None - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_1"].shape == (1, 2048, 2048) - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_1"].dtype == np.uint16 - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_x_pixel_size"].shape == (1,) - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_x_pixel_size"].dtype == np.uint8 - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_y_pixel_size"].shape == (1,) - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_y_pixel_size"].dtype == np.uint8 - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_detector_distance"].shape == (1,) - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_detector_distance"].dtype == np.float32 - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_incident_wavelength"].shape == (1,) - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_incident_wavelength"].dtype == np.float32 - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_frame_time"].shape == (1,) - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_frame_time"].dtype == np.float32 - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_beam_center_x"].shape == (1,) - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_beam_center_x"].dtype == np.uint8 - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_beam_center_y"].shape == (1,) - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_beam_center_y"].dtype == np.uint8 - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_count_time"].shape == (1,) - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_count_time"].dtype == np.float32 - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_pixel_mask"].shape == (1, 2048, 2048) - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_pixel_mask"].dtype == np.uint8 + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_1"].shape == ( + 1, + 2048, + 2048, + ) + assert ( + tiled_client.values().last()["streams"]["primary"]["test_eiger_1"].dtype + == np.uint16 + ) + assert tiled_client.values().last()["streams"]["primary"][ + "test_eiger_x_pixel_size" + ].shape == (1,) + assert ( + tiled_client.values() + .last()["streams"]["primary"]["test_eiger_x_pixel_size"] + .dtype + == np.uint8 + ) + assert tiled_client.values().last()["streams"]["primary"][ + "test_eiger_y_pixel_size" + ].shape == (1,) + assert ( + tiled_client.values() + .last()["streams"]["primary"]["test_eiger_y_pixel_size"] + .dtype + == np.uint8 + ) + assert tiled_client.values().last()["streams"]["primary"][ + "test_eiger_detector_distance" + ].shape == (1,) + assert ( + tiled_client.values() + .last()["streams"]["primary"]["test_eiger_detector_distance"] + .dtype + == np.float32 + ) + assert tiled_client.values().last()["streams"]["primary"][ + "test_eiger_incident_wavelength" + ].shape == (1,) + assert ( + tiled_client.values() + .last()["streams"]["primary"]["test_eiger_incident_wavelength"] + .dtype + == np.float32 + ) + assert tiled_client.values().last()["streams"]["primary"][ + "test_eiger_frame_time" + ].shape == (1,) + assert ( + tiled_client.values() + .last()["streams"]["primary"]["test_eiger_frame_time"] + .dtype + == np.float32 + ) + assert tiled_client.values().last()["streams"]["primary"][ + "test_eiger_beam_center_x" + ].shape == (1,) + assert ( + tiled_client.values() + .last()["streams"]["primary"]["test_eiger_beam_center_x"] + .dtype + == np.uint8 + ) + assert tiled_client.values().last()["streams"]["primary"][ + "test_eiger_beam_center_y" + ].shape == (1,) + assert ( + tiled_client.values() + .last()["streams"]["primary"]["test_eiger_beam_center_y"] + .dtype + == np.uint8 + ) + assert tiled_client.values().last()["streams"]["primary"][ + "test_eiger_count_time" + ].shape == (1,) + assert ( + tiled_client.values() + .last()["streams"]["primary"]["test_eiger_count_time"] + .dtype + == np.float32 + ) + assert tiled_client.values().last()["streams"]["primary"][ + "test_eiger_pixel_mask" + ].shape == (1, 2048, 2048) + assert ( + tiled_client.values() + .last()["streams"]["primary"]["test_eiger_pixel_mask"] + .dtype + == np.uint8 + ) set_mock_value(mock_eiger_detector.fileio.sequence_id, 2) set_mock_value(mock_eiger_detector.driver.num_images, 1) uid = RE(_count_plan([mock_eiger_detector], num=10, num_images=1, sequence_id=2)) assert uid is not None - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_1"].shape == (10, 2048, 2048) - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_1"].dtype == np.uint16 - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_x_pixel_size"].shape == (10,) - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_x_pixel_size"].dtype == np.uint8 - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_y_pixel_size"].shape == (10,) - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_y_pixel_size"].dtype == np.uint8 - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_detector_distance"].shape == (10,) - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_detector_distance"].dtype == np.float32 - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_incident_wavelength"].shape == (10,) - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_incident_wavelength"].dtype == np.float32 - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_frame_time"].shape == (10,) - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_frame_time"].dtype == np.float32 - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_beam_center_x"].shape == (10,) - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_beam_center_x"].dtype == np.uint8 - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_beam_center_y"].shape == (10,) - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_beam_center_y"].dtype == np.uint8 - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_count_time"].shape == (10,) - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_count_time"].dtype == np.float32 - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_pixel_mask"].shape == (10, 2048, 2048) - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_pixel_mask"].dtype == np.uint8 + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_1"].shape == ( + 10, + 2048, + 2048, + ) + assert ( + tiled_client.values().last()["streams"]["primary"]["test_eiger_1"].dtype + == np.uint16 + ) + assert tiled_client.values().last()["streams"]["primary"][ + "test_eiger_x_pixel_size" + ].shape == (10,) + assert ( + tiled_client.values() + .last()["streams"]["primary"]["test_eiger_x_pixel_size"] + .dtype + == np.uint8 + ) + assert tiled_client.values().last()["streams"]["primary"][ + "test_eiger_y_pixel_size" + ].shape == (10,) + assert ( + tiled_client.values() + .last()["streams"]["primary"]["test_eiger_y_pixel_size"] + .dtype + == np.uint8 + ) + assert tiled_client.values().last()["streams"]["primary"][ + "test_eiger_detector_distance" + ].shape == (10,) + assert ( + tiled_client.values() + .last()["streams"]["primary"]["test_eiger_detector_distance"] + .dtype + == np.float32 + ) + assert tiled_client.values().last()["streams"]["primary"][ + "test_eiger_incident_wavelength" + ].shape == (10,) + assert ( + tiled_client.values() + .last()["streams"]["primary"]["test_eiger_incident_wavelength"] + .dtype + == np.float32 + ) + assert tiled_client.values().last()["streams"]["primary"][ + "test_eiger_frame_time" + ].shape == (10,) + assert ( + tiled_client.values() + .last()["streams"]["primary"]["test_eiger_frame_time"] + .dtype + == np.float32 + ) + assert tiled_client.values().last()["streams"]["primary"][ + "test_eiger_beam_center_x" + ].shape == (10,) + assert ( + tiled_client.values() + .last()["streams"]["primary"]["test_eiger_beam_center_x"] + .dtype + == np.uint8 + ) + assert tiled_client.values().last()["streams"]["primary"][ + "test_eiger_beam_center_y" + ].shape == (10,) + assert ( + tiled_client.values() + .last()["streams"]["primary"]["test_eiger_beam_center_y"] + .dtype + == np.uint8 + ) + assert tiled_client.values().last()["streams"]["primary"][ + "test_eiger_count_time" + ].shape == (10,) + assert ( + tiled_client.values() + .last()["streams"]["primary"]["test_eiger_count_time"] + .dtype + == np.float32 + ) + assert tiled_client.values().last()["streams"]["primary"][ + "test_eiger_pixel_mask" + ].shape == (10, 2048, 2048) + assert ( + tiled_client.values() + .last()["streams"]["primary"]["test_eiger_pixel_mask"] + .dtype + == np.uint8 + ) From 8d485e56d6b881f66360bd4479ac9c6095f177fe Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Fri, 11 Jul 2025 11:16:08 -0400 Subject: [PATCH 16/46] Update signals to reflect actual values --- src/cditools/eiger_async.py | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index 251ed1eb..598432e5 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -47,11 +47,11 @@ class EigerTriggerMode(StrictEnum): See https://areadetector.github.io/areaDetector/ADEiger/eiger.html#implementation-of-standard-driver-parameters """ - INTERNAL_SERIES = "ints" - INTERNAL_ENABLE = "int" - EXTERNAL_SERIES = "exts" - EXTERNAL_ENABLE = "ext" - EXTERNAL_GATE = "gate" + INTERNAL_SERIES = "Internal Series" + INTERNAL_ENABLE = "Internal Enable" + EXTERNAL_SERIES = "External Series" + EXTERNAL_ENABLE = "External Enable" + CONTINUOUS = "Continuous" class EigerExtGateMode(StrictEnum): @@ -70,7 +70,7 @@ class EigerROIMode(StrictEnum): See https://areadetector.github.io/areaDetector/ADEiger/eiger.html#readout-setup """ - DISABLED = "Disabled" + DISABLED = "Disable" _4M = "4M" @@ -80,10 +80,8 @@ class EigerCompressionAlgo(StrictEnum): See https://areadetector.github.io/areaDetector/ADEiger/eiger.html#readout-setup """ - LZ4 = "lz4" - BSLZ4 = "bslz4" - NONE = "None" - + LZ4 = "LZ4" + BSLZ4 = "BS LZ4" class EigerDataSource(StrictEnum): """Data sources for the Eiger detector. @@ -170,7 +168,7 @@ class EigerDriverIO(ADBaseIO): link1: A[SignalR[str], PvSuffix("Link1_RBV")] link2: A[SignalR[str], PvSuffix("Link2_RBV")] link3: A[SignalR[str], PvSuffix("Link3_RBV")] - dcu_buffer_free: A[SignalR[int], PvSuffix("DCUBufferFree_RBV")] + dcu_buffer_free: A[SignalR[float], PvSuffix("DCUBufferFree_RBV")] hv_reset_time: A[SignalRW[float], PvSuffix.rbv("HVResetTime")] hv_reset: A[SignalRW[bool], PvSuffix("HVReset", "HVReset")] hv_state: A[SignalR[str], PvSuffix("HVState_RBV")] @@ -217,7 +215,7 @@ class EigerDriverIO(ADBaseIO): ] stream_hdr_appendix: A[SignalRW[str], PvSuffix.rbv("StreamHdrAppendix")] stream_img_appendix: A[SignalRW[str], PvSuffix.rbv("StreamImgAppendix")] - stream_dropped: A[SignalR[float], PvSuffix("StreamDropped_RBV")] + stream_dropped: A[SignalR[int], PvSuffix("StreamDropped_RBV")] # Monitor Interface monitor_enable: A[SignalRW[bool], PvSuffix.rbv("MonitorEnable")] @@ -260,7 +258,7 @@ class EigerFileIO(NDFileIO): save_files: A[SignalRW[bool], PvSuffix.rbv("SaveFiles")] file_owner: A[SignalRW[str], PvSuffix.rbv("FileOwner")] file_owner_grp: A[SignalRW[str], PvSuffix.rbv("FileOwnerGrp")] - file_perms: A[SignalRW[str], PvSuffix.rbv("FilePerms")] + file_perms: A[SignalRW[float], PvSuffix.rbv("FilePerms")] fw_free: A[SignalR[float], PvSuffix("FWFree_RBV")] fw_auto_remove: A[SignalRW[bool], PvSuffix.rbv("FWAutoRemove")] fw_clear: A[SignalRW[float], PvSuffix("FWClear")] @@ -524,11 +522,6 @@ async def prepare(self, trigger_info: TriggerInfo) -> None: await self.driver.trigger_mode.set(EigerTriggerMode.INTERNAL_SERIES) elif trigger_info.trigger == DetectorTrigger.EDGE_TRIGGER: await self.driver.trigger_mode.set(EigerTriggerMode.EXTERNAL_SERIES) - elif trigger_info.trigger in [ - DetectorTrigger.VARIABLE_GATE, - DetectorTrigger.CONSTANT_GATE, - ]: - await self.driver.trigger_mode.set(EigerTriggerMode.EXTERNAL_GATE) else: msg = f"Trigger mode {trigger_info.trigger} not supported" raise NotImplementedError(msg) From e70e5d4e0795b40e20e974a9d63cfed8d5ae3abd Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Mon, 14 Jul 2025 14:59:46 -0400 Subject: [PATCH 17/46] Separate PVs for Eiger1, Eiger2; Add EigerDatasetDescription; Add logic back for nimgs_per_file --- pixi.lock | 4 +- src/cditools/eiger_async.py | 222 +++++++++++++++++++++++------------- 2 files changed, 147 insertions(+), 79 deletions(-) diff --git a/pixi.lock b/pixi.lock index e69a24bf..15244eff 100644 --- a/pixi.lock +++ b/pixi.lock @@ -996,8 +996,8 @@ packages: requires_python: '>=3.8' - pypi: . name: cditools - version: 0.1.dev49+ga136df1.d20250710 - sha256: e49a6d9c33ef6c8f529bf8e6538f503155c3fa5dab0cf30261a0f1fb99086aaf + version: 0.1.dev51+g8d485e5.d20250711 + sha256: 47147c08df0ec8e2f3c889509e67cbae917d417f2aa55ae913d8cc7e404e5f56 requires_dist: - ophyd - ophyd-async>=0.10.0a4 diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index 598432e5..b9541cfc 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -8,6 +8,7 @@ from collections.abc import AsyncIterator, Sequence from logging import getLogger from pathlib import Path +from math import ceil from typing import Annotated as A from typing import Any, cast @@ -16,7 +17,6 @@ from event_model import DataKey # type: ignore[import-untyped] from ophyd_async.core import ( AsyncStatus, - DatasetDescriber, DetectorTrigger, HDFDatasetDescription, HDFDocumentComposer, @@ -33,14 +33,35 @@ ADImageMode, ADWriter, AreaDetector, - NDFileIO, NDPluginBaseIO, + NDArrayBaseIO, + ADFileWriteMode, + ADBaseDatasetDescriber ) from ophyd_async.epics.signal import PvSuffix logger = getLogger(__name__) +# TODO: Port to ophyd-async (https://github.com/bluesky/ophyd-async/issues/961) +# ============================================================================== +class NDFileIO(NDArrayBaseIO): + file_path: A[SignalRW[str], PvSuffix.rbv("FilePath")] + file_name: A[SignalRW[str], PvSuffix.rbv("FileName")] + file_path_exists: A[SignalR[bool], PvSuffix("FilePathExists_RBV")] + file_template: A[SignalRW[str], PvSuffix.rbv("FileTemplate")] + full_file_name: A[SignalR[str], PvSuffix("FullFileName_RBV")] + file_number: A[SignalRW[int], PvSuffix("FileNumber")] + auto_increment: A[SignalRW[bool], PvSuffix("AutoIncrement")] + file_write_mode: A[SignalRW[ADFileWriteMode], PvSuffix.rbv("FileWriteMode")] + num_capture: A[SignalRW[int], PvSuffix.rbv("NumCapture")] + num_captured: A[SignalR[int], PvSuffix("NumCaptured_RBV")] + capture: A[SignalRW[bool], PvSuffix.rbv("Capture")] + array_size0: A[SignalR[int], PvSuffix("ArraySize0")] + array_size1: A[SignalR[int], PvSuffix("ArraySize1")] + create_directory: A[SignalRW[int], PvSuffix("CreateDirectory")] +# ============================================================================== + class EigerTriggerMode(StrictEnum): """Trigger modes for the Eiger detector. @@ -139,17 +160,17 @@ class EigerDriverIO(ADBaseIO): acquire_time: A[SignalRW[float], PvSuffix.rbv("AcquireTime")] acquire_period: A[SignalRW[float], PvSuffix.rbv("AcquirePeriod")] data_type: A[SignalRW[str], PvSuffix.rbv("DataType")] - temperature_actual: A[SignalRW[float], PvSuffix.rbv("TemperatureActual")] - max_size_x: A[SignalRW[int], PvSuffix.rbv("MaxSizeX")] - max_size_y: A[SignalRW[int], PvSuffix.rbv("MaxSizeY")] - array_size_x: A[SignalRW[int], PvSuffix.rbv("ArraySizeX")] - array_size_y: A[SignalRW[int], PvSuffix.rbv("ArraySizeY")] - manufacturer: A[SignalRW[str], PvSuffix.rbv("Manufacturer")] - model: A[SignalRW[str], PvSuffix.rbv("Model")] - serial_number: A[SignalRW[str], PvSuffix.rbv("SerialNumber")] - firmware_version: A[SignalRW[str], PvSuffix.rbv("FirmwareVersion")] - sdk_version: A[SignalRW[str], PvSuffix.rbv("SDKVersion")] - driver_version: A[SignalRW[str], PvSuffix.rbv("DriverVersion")] + temperature_actual: A[SignalR[float], PvSuffix("TemperatureActual")] + max_size_x: A[SignalR[int], PvSuffix("MaxSizeX_RBV")] + max_size_y: A[SignalR[int], PvSuffix("MaxSizeY_RBV")] + array_size_x: A[SignalR[int], PvSuffix("ArraySizeX_RBV")] + array_size_y: A[SignalR[int], PvSuffix("ArraySizeY_RBV")] + manufacturer: A[SignalR[str], PvSuffix("Manufacturer_RBV")] + model: A[SignalR[str], PvSuffix("Model_RBV")] + serial_number: A[SignalR[str], PvSuffix("SerialNumber_RBV")] + firmware_version: A[SignalR[str], PvSuffix("FirmwareVersion_RBV")] + sdk_version: A[SignalR[str], PvSuffix("SDKVersion_RBV")] + driver_version: A[SignalR[str], PvSuffix("DriverVersion_RBV")] # Detector Information description: A[SignalR[str], PvSuffix("Description_RBV")] @@ -169,26 +190,15 @@ class EigerDriverIO(ADBaseIO): link2: A[SignalR[str], PvSuffix("Link2_RBV")] link3: A[SignalR[str], PvSuffix("Link3_RBV")] dcu_buffer_free: A[SignalR[float], PvSuffix("DCUBufferFree_RBV")] - hv_reset_time: A[SignalRW[float], PvSuffix.rbv("HVResetTime")] - hv_reset: A[SignalRW[bool], PvSuffix("HVReset", "HVReset")] - hv_state: A[SignalR[str], PvSuffix("HVState_RBV")] # Acquisition Setup - threshold: A[SignalRW[float], PvSuffix.rbv("Threshold")] - threshold1_enable: A[SignalRW[bool], PvSuffix.rbv("Threshold1Enable")] - threshold2: A[SignalRW[float], PvSuffix.rbv("Threshold2")] - threshold2_enable: A[SignalRW[bool], PvSuffix.rbv("Threshold2Enable")] - threshold_diff_enable: A[SignalRW[bool], PvSuffix.rbv("ThresholdDiffEnable")] photon_energy: A[SignalRW[float], PvSuffix.rbv("PhotonEnergy")] - counting_mode: A[SignalRW[str], PvSuffix.rbv("CountingMode")] # Trigger Setup - ext_gate_mode: A[SignalRW[str], PvSuffix.rbv("ExtGateMode")] trigger: A[SignalRW[float], PvSuffix("Trigger")] trigger_exposure: A[SignalRW[float], PvSuffix.rbv("TriggerExposure")] num_triggers: A[SignalRW[int], PvSuffix.rbv("NumTriggers")] manual_trigger: A[SignalRW[bool], PvSuffix.rbv("ManualTrigger")] - trigger_start_delay: A[SignalRW[float], PvSuffix.rbv("TriggerStartDelay")] # Readout Setup roi_mode: A[SignalRW[EigerROIMode], PvSuffix.rbv("ROIMode")] @@ -196,7 +206,6 @@ class EigerDriverIO(ADBaseIO): countrate_corr_applied: A[SignalRW[bool], PvSuffix.rbv("CountrateCorrApplied")] pixel_mask_applied: A[SignalRW[bool], PvSuffix.rbv("PixelMaskApplied")] auto_summation: A[SignalRW[bool], PvSuffix.rbv("AutoSummation")] - signed_data: A[SignalRW[bool], PvSuffix.rbv("SignedData")] compression_algo: A[SignalRW[EigerCompressionAlgo], PvSuffix.rbv("CompressionAlgo")] data_source: A[SignalRW[EigerDataSource], PvSuffix.rbv("DataSource")] @@ -208,13 +217,10 @@ class EigerDriverIO(ADBaseIO): # Stream Interface stream_enable: A[SignalRW[bool], PvSuffix.rbv("StreamEnable")] stream_state: A[SignalR[str], PvSuffix("StreamState_RBV")] - stream_version: A[SignalRW[EigerStreamVersion], PvSuffix.rbv("StreamVersion")] stream_decompress: A[SignalRW[bool], PvSuffix.rbv("StreamDecompress")] stream_hdr_detail: A[ SignalRW[EigerStreamHdrDetail], PvSuffix.rbv("StreamHdrDetail") ] - stream_hdr_appendix: A[SignalRW[str], PvSuffix.rbv("StreamHdrAppendix")] - stream_img_appendix: A[SignalRW[str], PvSuffix.rbv("StreamImgAppendix")] stream_dropped: A[SignalR[int], PvSuffix("StreamDropped_RBV")] # Monitor Interface @@ -245,14 +251,41 @@ class EigerDriverIO(ADBaseIO): energy_eps: A[SignalRW[float], PvSuffix.rbv("EnergyEps")] +class Eiger2DriverIO(EigerDriverIO): + """Eiger2 driver interface.""" + + # Detector Status + hv_reset_time: A[SignalRW[float], PvSuffix.rbv("HVResetTime")] + hv_reset: A[SignalRW[bool], PvSuffix("HVReset", "HVReset")] + hv_state: A[SignalR[str], PvSuffix("HVState_RBV")] + + # Acquisition Setup + threshold: A[SignalRW[float], PvSuffix.rbv("Threshold")] + threshold1_enable: A[SignalRW[bool], PvSuffix.rbv("Threshold1Enable")] + threshold2: A[SignalRW[float], PvSuffix.rbv("Threshold2")] + threshold2_enable: A[SignalRW[bool], PvSuffix.rbv("Threshold2Enable")] + threshold_diff_enable: A[SignalRW[bool], PvSuffix.rbv("ThresholdDiffEnable")] + counting_mode: A[SignalRW[str], PvSuffix.rbv("CountingMode")] + + # Trigger Setup + ext_gate_mode: A[SignalRW[str], PvSuffix.rbv("ExtGateMode")] + trigger_start_delay: A[SignalRW[float], PvSuffix.rbv("TriggerStartDelay")] + + # Readout Setup + signed_data: A[SignalRW[bool], PvSuffix.rbv("SignedData")] + + # Stream Interface + stream_version: A[SignalRW[EigerStreamVersion], PvSuffix.rbv("StreamVersion")] + stream_hdr_appendix: A[SignalRW[str], PvSuffix.rbv("StreamHdrAppendix")] + stream_img_appendix: A[SignalRW[str], PvSuffix.rbv("StreamImgAppendix")] + + class EigerFileIO(NDFileIO): """FileWriter interface for the Eiger detector.""" fw_enable: A[SignalRW[bool], PvSuffix.rbv("FWEnable")] fw_state: A[SignalR[str], PvSuffix("FWState_RBV")] - fw_hdf5_format: A[SignalRW[EigerHDF5Format], PvSuffix.rbv("FWHDF5Format")] fw_compression: A[SignalRW[bool], PvSuffix.rbv("FWCompression")] - fw_nimgs_per_file: A[SignalRW[float], PvSuffix.rbv("FWNImgsPerFile")] fw_name_pattern: A[SignalRW[str], PvSuffix.rbv("FWNamePattern")] sequence_id: A[SignalR[int], PvSuffix("SequenceId")] save_files: A[SignalRW[bool], PvSuffix.rbv("SaveFiles")] @@ -264,18 +297,36 @@ class EigerFileIO(NDFileIO): fw_clear: A[SignalRW[float], PvSuffix("FWClear")] +class Eiger2FileIO(EigerFileIO): + """FileWriter interface for the Eiger2 detector.""" + + fw_hdf5_format: A[SignalRW[EigerHDF5Format], PvSuffix.rbv("FWHDF5Format")] + fw_nimgs_per_file: A[SignalRW[int], PvSuffix.rbv("FWNImgsPerFile")] + + +class EigerDatasetDescriber(ADBaseDatasetDescriber): + """Dataset describer for the Eiger detector.""" + + def __init__(self, driver: EigerDriverIO) -> None: + super().__init__(driver) + + async def num_triggers(self) -> int: + """Get the number of triggers.""" + if not isinstance(self._driver, EigerDriverIO): + raise ValueError("Driver is not an Eiger driver") + return await self._driver.num_triggers.get_value() + + class EigerWriter(ADWriter[EigerFileIO]): """Eiger-specific file writer using the built-in FileWriter interface.""" default_suffix: str = "cam1:" - # Forced minimum number of images per file to force a single HDF5 file - _min_num_images_per_file: int = 1e8 def __init__( self, fileio: EigerFileIO, path_provider: PathProvider, - dataset_describer: DatasetDescriber, + dataset_describer: EigerDatasetDescriber, plugins: dict[str, NDPluginBaseIO] | None = None, ): super().__init__( @@ -293,23 +344,27 @@ def __init__( self._old_fwn_num_images_per_file: int | None = None self._composer: HDFDocumentComposer | None = None + @classmethod + def with_io( + cls: type[EigerWriter], + prefix: str, + path_provider: PathProvider, + dataset_source: EigerDriverIO | None = None, + fileio_suffix: str | None = None, + ) -> EigerWriter: + if not isinstance(dataset_source, EigerDriverIO): + raise ValueError("Dataset source must be an Eiger driver") + + writer = super().with_io(prefix, path_provider, dataset_source, fileio_suffix) + writer._dataset_describer = EigerDatasetDescriber(dataset_source) + return writer + async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataKey]: """Setup file writing for acquisition.""" # Get file path info from path provider self._file_info = self._path_provider() self._current_sequence_id = await self.fileio.sequence_id.get_value() - # Force the number of images per file to a large number to simplify the logic - if self._old_fwn_num_images_per_file is None: - self._old_fwn_num_images_per_file = ( - await self.fileio.fw_nimgs_per_file.get_value() - ) - await self.fileio.fw_nimgs_per_file.set(self._min_num_images_per_file) - logger.warning( - "Setting fw_nimgs_per_file to %d to force writing to a single HDF5 file", - self._min_num_images_per_file, - ) - # Set the name pattern with $id replacement similar to original name_pattern = f"{self._file_info.filename}_$id" @@ -319,10 +374,19 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK self.fileio.fw_name_pattern.set(name_pattern), self.fileio.fw_enable.set(True), self.fileio.save_files.set(True), - self.fileio.fw_hdf5_format.set(EigerHDF5Format.LEGACY), ) - detector_shape, np_dtype = await asyncio.gather( + if isinstance(self.fileio, Eiger2FileIO): + await self.fileio.fw_hdf5_format.set(EigerHDF5Format.LEGACY) + num_images_per_file = await self.fileio.fw_nimgs_per_file.get_value() + else: + num_images_per_file = 1_000_000_000 + + if not isinstance(self._dataset_describer, EigerDatasetDescriber): + raise ValueError("Dataset describer is not an Eiger dataset describer") + + num_triggers, detector_shape, np_dtype = await asyncio.gather( + self._dataset_describer.num_triggers(), self._dataset_describer.shape(), self._dataset_describer.np_datatype(), ) @@ -398,45 +462,38 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK # Cache for use later self._exposures_per_event = exposures_per_event - # TODO: Add back once we allow setting `num_images_per_file` again - # # Add the array datasets (linked from the master file) - # # Linked keys are of the form - # # - "/entry/data_000001" - # # - "/entry/data_000002" - # # - ... - # # Example: if exposures_per_event (num_images) is 60, num_triggers is 2, and num_images_per_file is 100, - # # then the data_000001 file will have 100 images and the data_000002 filewill have 20 images. - # # Put simply, the last file could have less than num_images_per_file images. - # total_images = num_triggers * exposures_per_event - # frame_datasets = [ - # HDFDatasetDescription( - # data_key=f"{name}_{i}", - # dataset=f"/entry/data_{i:06d}", - # shape=( - # min( - # num_images_per_file, - # total_images - (i - 1) * num_images_per_file, - # ), - # *detector_shape, - # ), - # dtype_numpy=np_dtype, - # chunk_shape=(1, *detector_shape), - # ) - # for i in range(1, ceil(total_images / num_images_per_file) + 1) - # ] + # Add the array datasets (linked from the master file) + # Linked keys are of the form + # - "/entry/data_000001" + # - "/entry/data_000002" + # - ... + # Example: if exposures_per_event (num_images) is 60, num_triggers is 2, and num_images_per_file is 100, + # then the data_000001 file will have 100 images and the data_000002 filewill have 20 images. + # Put simply, the last file could have less than num_images_per_file images. + total_images = num_triggers * exposures_per_event frame_datasets = [ HDFDatasetDescription( - data_key=f"{name}_1", - dataset=f"/entry/data_{1:06d}", - shape=(exposures_per_event, *detector_shape), + data_key=f"{name}_{i}", + dataset=f"/entry/data_{i:06d}", + shape=( + min( + num_images_per_file, + total_images - (i - 1) * num_images_per_file, + ), + *detector_shape, + ), dtype_numpy=np_dtype, chunk_shape=(1, *detector_shape), ) + for i in range(1, ceil(total_images / num_images_per_file) + 1) ] self._datasets = master_datasets + frame_datasets logger.warning("OPENING FILE") + if self._master_file_path is None: + raise ValueError("Master file path is None") + return { ds.data_key: DataKey( source=self._master_file_path.as_posix(), @@ -470,6 +527,9 @@ async def collect_stream_docs( """Generate stream documents for the written HDF5 files.""" if indices_written: if not self._composer: + if self._master_file_path is None: + raise ValueError("Master file path is not set") + self._composer = HDFDocumentComposer( self._master_file_path, self._datasets, @@ -488,7 +548,6 @@ async def close(self) -> None: await asyncio.gather( self.fileio.fw_enable.set(False), self.fileio.save_files.set(False), - self.fileio.fw_nimgs_per_file.set(self._old_fwn_num_images_per_file), ) if self._master_file_path is not None and not self._master_file_path.exists(): @@ -531,12 +590,20 @@ async def prepare(self, trigger_info: TriggerInfo) -> None: else: image_mode = ADImageMode.MULTIPLE + if isinstance(trigger_info.number_of_events, list): + self._number_of_events_iter = iter(trigger_info.number_of_events) + else: + self._number_of_events_iter = iter([trigger_info.number_of_events]) + await asyncio.gather( - self.driver.num_triggers.set(trigger_info.number_of_events), self.driver.num_images.set(trigger_info.exposures_per_event), self.driver.image_mode.set(image_mode), ) + async def arm(self) -> None: + await self.driver.num_triggers.set(next(self._number_of_events_iter)) + await super().arm() + class EigerDetector(AreaDetector[EigerController]): """Eiger detector implementation using the AreaDetector pattern.""" @@ -572,6 +639,7 @@ def __init__( # TODO: Fix typing in base class self.driver: EigerDriverIO = cast(EigerDriverIO, self.driver) + self.fileio: EigerFileIO = cast(EigerFileIO, self.fileio) @AsyncStatus.wrap async def stage(self) -> None: From f67f22f55a4c20d9dd827ad0f7468b6c575215ac Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Mon, 14 Jul 2025 15:20:12 -0400 Subject: [PATCH 18/46] Add plugins argument --- src/cditools/eiger_async.py | 5 +++-- tests/test_eiger_async.py | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index b9541cfc..179803c8 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -349,13 +349,14 @@ def with_io( cls: type[EigerWriter], prefix: str, path_provider: PathProvider, - dataset_source: EigerDriverIO | None = None, + dataset_source: NDArrayBaseIO | None = None, fileio_suffix: str | None = None, + plugins: dict[str, NDPluginBaseIO] | None = None, ) -> EigerWriter: if not isinstance(dataset_source, EigerDriverIO): raise ValueError("Dataset source must be an Eiger driver") - writer = super().with_io(prefix, path_provider, dataset_source, fileio_suffix) + writer = super().with_io(prefix, path_provider, dataset_source, fileio_suffix, plugins) writer._dataset_describer = EigerDatasetDescriber(dataset_source) return writer diff --git a/tests/test_eiger_async.py b/tests/test_eiger_async.py index aba76e66..77f52c7e 100644 --- a/tests/test_eiger_async.py +++ b/tests/test_eiger_async.py @@ -189,7 +189,6 @@ async def test_eiger_writer_open( # Case 1: 1 image per file, 1 image, 1 trigger set_mock_value(mock_eiger_fileio.sequence_id, 1) - set_mock_value(mock_eiger_fileio.fw_nimgs_per_file, 1) set_mock_value(mock_eiger_driver.num_images, 1) set_mock_value(mock_eiger_driver.num_triggers, 1) From ee10d29052ceaf34c18b3bf1ff8aac1f8d215a19 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Mon, 14 Jul 2025 16:35:21 -0400 Subject: [PATCH 19/46] Added back forcing min images per file --- src/cditools/eiger_async.py | 83 ++++++++----------------------------- tests/test_eiger_async.py | 45 +++++++++----------- 2 files changed, 36 insertions(+), 92 deletions(-) diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index 179803c8..ee880f0a 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -8,7 +8,6 @@ from collections.abc import AsyncIterator, Sequence from logging import getLogger from pathlib import Path -from math import ceil from typing import Annotated as A from typing import Any, cast @@ -45,7 +44,7 @@ # TODO: Port to ophyd-async (https://github.com/bluesky/ophyd-async/issues/961) # ============================================================================== -class NDFileIO(NDArrayBaseIO): +class NDFileIO(NDPluginBaseIO): file_path: A[SignalRW[str], PvSuffix.rbv("FilePath")] file_name: A[SignalRW[str], PvSuffix.rbv("FileName")] file_path_exists: A[SignalR[bool], PvSuffix("FilePathExists_RBV")] @@ -304,29 +303,18 @@ class Eiger2FileIO(EigerFileIO): fw_nimgs_per_file: A[SignalRW[int], PvSuffix.rbv("FWNImgsPerFile")] -class EigerDatasetDescriber(ADBaseDatasetDescriber): - """Dataset describer for the Eiger detector.""" - - def __init__(self, driver: EigerDriverIO) -> None: - super().__init__(driver) - - async def num_triggers(self) -> int: - """Get the number of triggers.""" - if not isinstance(self._driver, EigerDriverIO): - raise ValueError("Driver is not an Eiger driver") - return await self._driver.num_triggers.get_value() - - class EigerWriter(ADWriter[EigerFileIO]): """Eiger-specific file writer using the built-in FileWriter interface.""" default_suffix: str = "cam1:" + # Forced minimum number of images per file to force a single HDF5 file + _min_num_images_per_file: int = 1_000_000_000 def __init__( self, fileio: EigerFileIO, path_provider: PathProvider, - dataset_describer: EigerDatasetDescriber, + dataset_describer: ADBaseDatasetDescriber, plugins: dict[str, NDPluginBaseIO] | None = None, ): super().__init__( @@ -341,24 +329,8 @@ def __init__( self._file_info: PathInfo | None = None self._datasets: list[HDFDatasetDescription] = [] self._current_sequence_id: int | None = None - self._old_fwn_num_images_per_file: int | None = None self._composer: HDFDocumentComposer | None = None - @classmethod - def with_io( - cls: type[EigerWriter], - prefix: str, - path_provider: PathProvider, - dataset_source: NDArrayBaseIO | None = None, - fileio_suffix: str | None = None, - plugins: dict[str, NDPluginBaseIO] | None = None, - ) -> EigerWriter: - if not isinstance(dataset_source, EigerDriverIO): - raise ValueError("Dataset source must be an Eiger driver") - - writer = super().with_io(prefix, path_provider, dataset_source, fileio_suffix, plugins) - writer._dataset_describer = EigerDatasetDescriber(dataset_source) - return writer async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataKey]: """Setup file writing for acquisition.""" @@ -379,15 +351,14 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK if isinstance(self.fileio, Eiger2FileIO): await self.fileio.fw_hdf5_format.set(EigerHDF5Format.LEGACY) - num_images_per_file = await self.fileio.fw_nimgs_per_file.get_value() - else: - num_images_per_file = 1_000_000_000 - - if not isinstance(self._dataset_describer, EigerDatasetDescriber): - raise ValueError("Dataset describer is not an Eiger dataset describer") + # Force the number of images per file to a large number to simplify the logic + await self.fileio.fw_nimgs_per_file.set(self._min_num_images_per_file) + logger.warning( + "Setting fw_nimgs_per_file to %d to force writing to a single HDF5 file", + self._min_num_images_per_file, + ) - num_triggers, detector_shape, np_dtype = await asyncio.gather( - self._dataset_describer.num_triggers(), + detector_shape, np_dtype = await asyncio.gather( self._dataset_describer.shape(), self._dataset_describer.np_datatype(), ) @@ -463,30 +434,14 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK # Cache for use later self._exposures_per_event = exposures_per_event - # Add the array datasets (linked from the master file) - # Linked keys are of the form - # - "/entry/data_000001" - # - "/entry/data_000002" - # - ... - # Example: if exposures_per_event (num_images) is 60, num_triggers is 2, and num_images_per_file is 100, - # then the data_000001 file will have 100 images and the data_000002 filewill have 20 images. - # Put simply, the last file could have less than num_images_per_file images. - total_images = num_triggers * exposures_per_event frame_datasets = [ HDFDatasetDescription( - data_key=f"{name}_{i}", - dataset=f"/entry/data_{i:06d}", - shape=( - min( - num_images_per_file, - total_images - (i - 1) * num_images_per_file, - ), - *detector_shape, - ), + data_key=f"{name}_image", + dataset=f"/entry/data_{1:06d}", + shape=(exposures_per_event, *detector_shape), dtype_numpy=np_dtype, chunk_shape=(1, *detector_shape), ) - for i in range(1, ceil(total_images / num_images_per_file) + 1) ] self._datasets = master_datasets + frame_datasets @@ -527,6 +482,7 @@ async def collect_stream_docs( ) -> AsyncIterator[StreamAsset]: """Generate stream documents for the written HDF5 files.""" if indices_written: + logger.warning(f"COLLECTING STREAM DOCS: {indices_written}") if not self._composer: if self._master_file_path is None: raise ValueError("Master file path is not set") @@ -557,7 +513,6 @@ async def close(self) -> None: self._composer = None self._file_info = None self._current_sequence_id = None - self._old_fwn_num_images_per_file = None class EigerController(ADBaseController[EigerDriverIO]): @@ -592,19 +547,15 @@ async def prepare(self, trigger_info: TriggerInfo) -> None: image_mode = ADImageMode.MULTIPLE if isinstance(trigger_info.number_of_events, list): - self._number_of_events_iter = iter(trigger_info.number_of_events) + logger.warning("Got a list for number of events, expected to be set up externally: %s", trigger_info.number_of_events) else: - self._number_of_events_iter = iter([trigger_info.number_of_events]) + await self.driver.num_triggers.set(trigger_info.number_of_events) await asyncio.gather( self.driver.num_images.set(trigger_info.exposures_per_event), self.driver.image_mode.set(image_mode), ) - async def arm(self) -> None: - await self.driver.num_triggers.set(next(self._number_of_events_iter)) - await super().arm() - class EigerDetector(AreaDetector[EigerController]): """Eiger detector implementation using the AreaDetector pattern.""" diff --git a/tests/test_eiger_async.py b/tests/test_eiger_async.py index 77f52c7e..ae5493a3 100644 --- a/tests/test_eiger_async.py +++ b/tests/test_eiger_async.py @@ -6,7 +6,7 @@ import asyncio import shutil -from collections.abc import Sequence +from collections.abc import Generator, Sequence from pathlib import Path import bluesky.plan_stubs as bps @@ -98,7 +98,7 @@ def write_eiger_hdf5_file( @pytest.fixture -def mock_eiger_detector(RE: RunEngine) -> EigerDetector: +def mock_eiger_detector(RE: RunEngine) -> Generator[EigerDetector, None, None]: path_provider = StaticPathProvider( StaticFilenameProvider("test_eiger"), directory_path="/tmp/test_data" ) @@ -132,7 +132,9 @@ def mock_eiger_driver(RE: RunEngine) -> EigerDriverIO: @pytest.fixture def mock_eiger_fileio(RE: RunEngine) -> EigerFileIO: with init_devices(mock=True): - return EigerFileIO("MOCK:EIGER:cam1:") + fileio = EigerFileIO("MOCK:EIGER:cam1:") + + return fileio @pytest.fixture @@ -187,7 +189,7 @@ async def test_eiger_writer_open( mock_eiger_driver.data_type.get_value(), ) - # Case 1: 1 image per file, 1 image, 1 trigger + # Case 1: 1 image, 1 trigger set_mock_value(mock_eiger_fileio.sequence_id, 1) set_mock_value(mock_eiger_driver.num_images, 1) set_mock_value(mock_eiger_driver.num_triggers, 1) @@ -195,7 +197,6 @@ async def test_eiger_writer_open( description = await eiger_writer.open(name="test_eiger", exposures_per_event=1) assert await mock_eiger_fileio.fw_enable.get_value() is True assert await mock_eiger_fileio.save_files.get_value() is True - assert await mock_eiger_fileio.fw_hdf5_format.get_value() == EigerHDF5Format.LEGACY assert description.keys() == { "test_eiger_y_pixel_size", "test_eiger_x_pixel_size", @@ -206,16 +207,15 @@ async def test_eiger_writer_open( "test_eiger_beam_center_y", "test_eiger_count_time", "test_eiger_pixel_mask", - "test_eiger_1", + "test_eiger_image", } assert ( - description["test_eiger_1"]["source"] == "/tmp/test_data/test_eiger_1_master.h5" + description["test_eiger_image"]["source"] == "/tmp/test_data/test_eiger_1_master.h5" ) # Case 2: 4 images per file, 11 images, 2 triggers # Expect 6 files, the first 5 will have 4 images, the last will have 2 set_mock_value(mock_eiger_fileio.sequence_id, 2) - set_mock_value(mock_eiger_fileio.fw_nimgs_per_file, 4) set_mock_value(mock_eiger_driver.num_images, 11) set_mock_value(mock_eiger_driver.num_triggers, 2) description = await eiger_writer.open( @@ -232,9 +232,9 @@ async def test_eiger_writer_open( "test_eiger_beam_center_y", "test_eiger_count_time", "test_eiger_pixel_mask", - "test_eiger_1", + "test_eiger_image", } - data_key = description["test_eiger_1"] + data_key = description["test_eiger_image"] assert tuple(data_key["shape"]) == (11, array_size_x, array_size_y) assert data_key["dtype"] == "array" assert data_key["dtype_numpy"] == np.dtype(data_type.lower()).str @@ -272,7 +272,6 @@ async def test_eiger_writer_get_indices_written( ): """Test getting the number of indices written.""" set_mock_value(mock_eiger_fileio.sequence_id, 1) - set_mock_value(mock_eiger_fileio.fw_nimgs_per_file, 1) # Case 1: 1 image, 1 trigger set_mock_value(mock_eiger_driver.num_triggers, 1) @@ -329,7 +328,6 @@ async def test_eiger_writer_observe_indices_written( ) -> None: """Test observing indices as they are written.""" set_mock_value(mock_eiger_fileio.sequence_id, 1) - set_mock_value(mock_eiger_fileio.fw_nimgs_per_file, 1) async def _simulate_writing_indices( num_images: int, num_triggers: int, acquire_time: float = 0.01 @@ -419,7 +417,6 @@ async def collect_docs(num_triggers: int): return resource_docs, data_docs set_mock_value(mock_eiger_fileio.sequence_id, 1) - set_mock_value(mock_eiger_fileio.fw_nimgs_per_file, 1) set_mock_value(mock_eiger_driver.num_images, 1) set_mock_value(mock_eiger_driver.num_triggers, 1) await eiger_writer.open(name="test_eiger", exposures_per_event=1) @@ -461,7 +458,6 @@ async def test_eiger_writer_close( set_mock_value(mock_eiger_fileio.sequence_id, 1) set_mock_value(mock_eiger_driver.num_images, 1) set_mock_value(mock_eiger_driver.num_triggers, 1) - set_mock_value(mock_eiger_fileio.fw_nimgs_per_file, 1) await eiger_writer.open(name="test_eiger", exposures_per_event=1) assert await mock_eiger_fileio.fw_enable.get_value() is True assert await mock_eiger_fileio.save_files.get_value() is True @@ -473,10 +469,8 @@ async def test_eiger_writer_close( assert eiger_writer._composer is None assert eiger_writer._current_sequence_id is None assert eiger_writer._file_info is None - assert await mock_eiger_fileio.fw_nimgs_per_file.get_value() == 1 -# TODO: Test the controller's overridden methods and do an integration test with bluesky plans + tiled readback @pytest.mark.asyncio async def test_eiger_controller_prepare(eiger_controller: EigerController) -> None: trigger_info = TriggerInfo( @@ -493,7 +487,7 @@ async def test_eiger_controller_prepare(eiger_controller: EigerController) -> No await eiger_controller.driver.trigger_mode.get_value() == EigerTriggerMode.INTERNAL_SERIES ) - assert await eiger_controller.driver.num_triggers.get_value() == 1 + #assert await eiger_controller.driver.num_triggers.get_value() == 1 assert await eiger_controller.driver.num_images.get_value() == 1 assert await eiger_controller.driver.image_mode.get_value() == ADImageMode.MULTIPLE @@ -511,7 +505,7 @@ async def test_eiger_controller_prepare(eiger_controller: EigerController) -> No await eiger_controller.driver.trigger_mode.get_value() == EigerTriggerMode.EXTERNAL_SERIES ) - assert await eiger_controller.driver.num_triggers.get_value() == 10 + #assert await eiger_controller.driver.num_triggers.get_value() == 10 assert await eiger_controller.driver.num_images.get_value() == 5 assert await eiger_controller.driver.image_mode.get_value() == ADImageMode.MULTIPLE @@ -519,7 +513,7 @@ async def test_eiger_controller_prepare(eiger_controller: EigerController) -> No number_of_events=0, livetime=None, deadtime=0.0, - trigger=DetectorTrigger.VARIABLE_GATE, + trigger=DetectorTrigger.EDGE_TRIGGER, exposure_timeout=10.0, exposures_per_event=1, ) @@ -527,9 +521,9 @@ async def test_eiger_controller_prepare(eiger_controller: EigerController) -> No assert await eiger_controller.driver.acquire_time.get_value() == 0.0 assert ( await eiger_controller.driver.trigger_mode.get_value() - == EigerTriggerMode.EXTERNAL_GATE + == EigerTriggerMode.EXTERNAL_SERIES ) - assert await eiger_controller.driver.num_triggers.get_value() == 0 + #assert await eiger_controller.driver.num_triggers.get_value() == 0 assert await eiger_controller.driver.num_images.get_value() == 1 assert ( await eiger_controller.driver.image_mode.get_value() == ADImageMode.CONTINUOUS @@ -636,16 +630,15 @@ def _count_plan( set_mock_value(mock_eiger_detector.fileio.sequence_id, 1) set_mock_value(mock_eiger_detector.driver.num_images, 1) - set_mock_value(mock_eiger_detector.driver.num_triggers, 1) uid = RE(_count_plan([mock_eiger_detector])) assert uid is not None - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_1"].shape == ( + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_image"].shape == ( 1, 2048, 2048, ) assert ( - tiled_client.values().last()["streams"]["primary"]["test_eiger_1"].dtype + tiled_client.values().last()["streams"]["primary"]["test_eiger_image"].dtype == np.uint16 ) assert tiled_client.values().last()["streams"]["primary"][ @@ -734,13 +727,13 @@ def _count_plan( set_mock_value(mock_eiger_detector.driver.num_images, 1) uid = RE(_count_plan([mock_eiger_detector], num=10, num_images=1, sequence_id=2)) assert uid is not None - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_1"].shape == ( + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_image"].shape == ( 10, 2048, 2048, ) assert ( - tiled_client.values().last()["streams"]["primary"]["test_eiger_1"].dtype + tiled_client.values().last()["streams"]["primary"]["test_eiger_image"].dtype == np.uint16 ) assert tiled_client.values().last()["streams"]["primary"][ From e4c43d871e208593a8a81c4eb2a559bc484ccf2f Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Mon, 14 Jul 2025 17:05:52 -0400 Subject: [PATCH 20/46] Need to debug detector tests --- src/cditools/eiger_async.py | 2 -- tests/test_eiger_async.py | 7 +++++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index ee880f0a..d02d4039 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -446,7 +446,6 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK self._datasets = master_datasets + frame_datasets - logger.warning("OPENING FILE") if self._master_file_path is None: raise ValueError("Master file path is None") @@ -501,7 +500,6 @@ async def collect_stream_docs( async def close(self) -> None: """Clean up file writing after acquisition and validate files exist.""" # Disable file writer and reset number of images per file - logger.warning("CLOSING FILE") await asyncio.gather( self.fileio.fw_enable.set(False), self.fileio.save_files.set(False), diff --git a/tests/test_eiger_async.py b/tests/test_eiger_async.py index ae5493a3..56e7ec87 100644 --- a/tests/test_eiger_async.py +++ b/tests/test_eiger_async.py @@ -534,7 +534,8 @@ async def test_eiger_controller_prepare(eiger_controller: EigerController) -> No async def test_eiger_detector(mock_eiger_detector: EigerDetector) -> None: set_mock_value(mock_eiger_detector.driver.num_images, 1) set_mock_value(mock_eiger_detector.driver.num_triggers, 2) - set_mock_value(mock_eiger_detector.driver.acquire_period, 0.01) + set_mock_value(mock_eiger_detector.driver.acquire_period, 0.001) + set_mock_value(mock_eiger_detector.fileio.num_captured, 0) async def _simulate_one_trigger(num_captured: int): await asyncio.sleep(await mock_eiger_detector.driver.acquire_period.get_value()) @@ -559,6 +560,7 @@ async def _simulate_one_trigger(num_captured: int): await mock_eiger_detector.read() await mock_eiger_detector.unstage() + set_mock_value(mock_eiger_detector.fileio.num_captured, 0) # Case 2 - Fly Scan: prepare, kickoff, complete await mock_eiger_detector.prepare( TriggerInfo( @@ -566,7 +568,7 @@ async def _simulate_one_trigger(num_captured: int): livetime=0.01, deadtime=0.001, trigger=DetectorTrigger.INTERNAL, - exposure_timeout=1.0, + exposure_timeout=10.0, exposures_per_event=1, ) ) @@ -578,6 +580,7 @@ async def _simulate_one_trigger(num_captured: int): async def test_eiger_detector_with_RE( RE: RunEngine, tiled_client: Container, mock_eiger_detector: EigerDetector ) -> None: + set_mock_value(mock_eiger_detector.fileio.num_captured, 0) acquire_period = 0.01 def _count_plan( From 6b1687721f8060734ed80bb3f929d58ebe38848e Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Tue, 15 Jul 2025 10:45:09 -0400 Subject: [PATCH 21/46] Tests pass with new Eiger organization --- pixi.lock | 4 +- pyproject.toml | 2 +- src/cditools/eiger_async.py | 17 +++-- tests/test_eiger_async.py | 122 ++++++++++++------------------------ 4 files changed, 55 insertions(+), 90 deletions(-) diff --git a/pixi.lock b/pixi.lock index 15244eff..5bd2d53b 100644 --- a/pixi.lock +++ b/pixi.lock @@ -996,8 +996,8 @@ packages: requires_python: '>=3.8' - pypi: . name: cditools - version: 0.1.dev51+g8d485e5.d20250711 - sha256: 47147c08df0ec8e2f3c889509e67cbae917d417f2aa55ae913d8cc7e404e5f56 + version: 0.1.dev55+ge4c43d8.d20250715 + sha256: a9151703cfcc98e3ec8b42f6f48a3d0b846f2cbe302adfad988cb1c3e94ab350 requires_dist: - ophyd - ophyd-async>=0.10.0a4 diff --git a/pyproject.toml b/pyproject.toml index d657c6f4..03a13fa6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,7 +106,7 @@ report.exclude_also = [ files = ["src", "tests"] python_version = "3.9" warn_unused_configs = true -strict = true +strict = true enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] warn_unreachable = true disallow_untyped_defs = false diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index d02d4039..9b604fc2 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -28,14 +28,13 @@ ) from ophyd_async.epics.adcore import ( ADBaseController, + ADBaseDatasetDescriber, ADBaseIO, + ADFileWriteMode, ADImageMode, ADWriter, AreaDetector, NDPluginBaseIO, - NDArrayBaseIO, - ADFileWriteMode, - ADBaseDatasetDescriber ) from ophyd_async.epics.signal import PvSuffix @@ -59,8 +58,11 @@ class NDFileIO(NDPluginBaseIO): array_size0: A[SignalR[int], PvSuffix("ArraySize0")] array_size1: A[SignalR[int], PvSuffix("ArraySize1")] create_directory: A[SignalRW[int], PvSuffix("CreateDirectory")] + + # ============================================================================== + class EigerTriggerMode(StrictEnum): """Trigger modes for the Eiger detector. @@ -103,6 +105,7 @@ class EigerCompressionAlgo(StrictEnum): LZ4 = "LZ4" BSLZ4 = "BS LZ4" + class EigerDataSource(StrictEnum): """Data sources for the Eiger detector. @@ -331,7 +334,6 @@ def __init__( self._current_sequence_id: int | None = None self._composer: HDFDocumentComposer | None = None - async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataKey]: """Setup file writing for acquisition.""" # Get file path info from path provider @@ -438,7 +440,7 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK HDFDatasetDescription( data_key=f"{name}_image", dataset=f"/entry/data_{1:06d}", - shape=(exposures_per_event, *detector_shape), + shape=(exposures_per_event, *detector_shape), dtype_numpy=np_dtype, chunk_shape=(1, *detector_shape), ) @@ -545,7 +547,10 @@ async def prepare(self, trigger_info: TriggerInfo) -> None: image_mode = ADImageMode.MULTIPLE if isinstance(trigger_info.number_of_events, list): - logger.warning("Got a list for number of events, expected to be set up externally: %s", trigger_info.number_of_events) + logger.warning( + "Got a list for number of events, expected to be set up externally: %s", + trigger_info.number_of_events, + ) else: await self.driver.num_triggers.set(trigger_info.number_of_events) diff --git a/tests/test_eiger_async.py b/tests/test_eiger_async.py index 56e7ec87..55047090 100644 --- a/tests/test_eiger_async.py +++ b/tests/test_eiger_async.py @@ -6,16 +6,15 @@ import asyncio import shutil -from collections.abc import Generator, Sequence +from collections.abc import Generator from pathlib import Path -import bluesky.plan_stubs as bps +import bluesky.plans as bp import h5py import numpy as np import pytest from bluesky.callbacks.tiled_writer import TiledWriter from bluesky.run_engine import RunEngine -from bluesky.utils import MsgGenerator from ophyd_async.core import ( DetectorTrigger, PathProvider, @@ -26,6 +25,7 @@ ) from ophyd_async.epics.adcore import ADBaseDatasetDescriber, ADImageMode from ophyd_async.testing import ( + callback_on_mock_put, set_mock_value, ) from tiled.client.container import Container @@ -36,7 +36,6 @@ EigerDetector, EigerDriverIO, EigerFileIO, - EigerHDF5Format, EigerTriggerMode, EigerWriter, ) @@ -210,7 +209,8 @@ async def test_eiger_writer_open( "test_eiger_image", } assert ( - description["test_eiger_image"]["source"] == "/tmp/test_data/test_eiger_1_master.h5" + description["test_eiger_image"]["source"] + == "/tmp/test_data/test_eiger_1_master.h5" ) # Case 2: 4 images per file, 11 images, 2 triggers @@ -237,32 +237,12 @@ async def test_eiger_writer_open( data_key = description["test_eiger_image"] assert tuple(data_key["shape"]) == (11, array_size_x, array_size_y) assert data_key["dtype"] == "array" + assert "dtype_numpy" in data_key assert data_key["dtype_numpy"] == np.dtype(data_type.lower()).str + assert "external" in data_key assert data_key["external"] == "STREAM:" assert data_key["source"] == "/tmp/test_data/test_eiger_2_master.h5" - # # TODO: Add back when nimages_per_file is no longer hardcoded - # "test_eiger_2", - # "test_eiger_3", - # "test_eiger_4", - # "test_eiger_5", - # "test_eiger_6", - # } - - # for i in range(1, 6): - # data_key = description[f"test_eiger_{i}"] - # assert tuple(data_key["shape"]) == (4, array_size_x, array_size_y) - # assert data_key["dtype"] == "array" - # assert data_key["dtype_numpy"] == np.dtype(data_type.lower()).str - # assert data_key["external"] == "STREAM:" - # assert data_key["source"] == "/tmp/test_data/test_eiger_2_master.h5" - # data_key = description["test_eiger_6"] - # assert tuple(data_key["shape"]) == (2, array_size_x, array_size_y) - # assert data_key["dtype"] == "array" - # assert data_key["dtype_numpy"] == np.dtype(data_type.lower()).str - # assert data_key["external"] == "STREAM:" - # assert data_key["source"] == "/tmp/test_data/test_eiger_2_master.h5" - @pytest.mark.asyncio async def test_eiger_writer_get_indices_written( @@ -487,7 +467,7 @@ async def test_eiger_controller_prepare(eiger_controller: EigerController) -> No await eiger_controller.driver.trigger_mode.get_value() == EigerTriggerMode.INTERNAL_SERIES ) - #assert await eiger_controller.driver.num_triggers.get_value() == 1 + # assert await eiger_controller.driver.num_triggers.get_value() == 1 assert await eiger_controller.driver.num_images.get_value() == 1 assert await eiger_controller.driver.image_mode.get_value() == ADImageMode.MULTIPLE @@ -505,7 +485,7 @@ async def test_eiger_controller_prepare(eiger_controller: EigerController) -> No await eiger_controller.driver.trigger_mode.get_value() == EigerTriggerMode.EXTERNAL_SERIES ) - #assert await eiger_controller.driver.num_triggers.get_value() == 10 + # assert await eiger_controller.driver.num_triggers.get_value() == 10 assert await eiger_controller.driver.num_images.get_value() == 5 assert await eiger_controller.driver.image_mode.get_value() == ADImageMode.MULTIPLE @@ -523,7 +503,7 @@ async def test_eiger_controller_prepare(eiger_controller: EigerController) -> No await eiger_controller.driver.trigger_mode.get_value() == EigerTriggerMode.EXTERNAL_SERIES ) - #assert await eiger_controller.driver.num_triggers.get_value() == 0 + # assert await eiger_controller.driver.num_triggers.get_value() == 0 assert await eiger_controller.driver.num_images.get_value() == 1 assert ( await eiger_controller.driver.image_mode.get_value() == ADImageMode.CONTINUOUS @@ -537,9 +517,12 @@ async def test_eiger_detector(mock_eiger_detector: EigerDetector) -> None: set_mock_value(mock_eiger_detector.driver.acquire_period, 0.001) set_mock_value(mock_eiger_detector.fileio.num_captured, 0) - async def _simulate_one_trigger(num_captured: int): + async def _simulate_one_trigger(value: bool, wait: bool) -> None: await asyncio.sleep(await mock_eiger_detector.driver.acquire_period.get_value()) - set_mock_value(mock_eiger_detector.fileio.num_captured, num_captured) + num_captured = await mock_eiger_detector.fileio.num_captured.get_value() + set_mock_value(mock_eiger_detector.fileio.num_captured, num_captured + 1) + + callback_on_mock_put(mock_eiger_detector.driver.acquire, _simulate_one_trigger) # Standalone methods await mock_eiger_detector.describe() @@ -550,13 +533,9 @@ async def _simulate_one_trigger(num_captured: int): await mock_eiger_detector.driver.data_source.get_value() == EigerDataSource.FILE_WRITER ) - status = mock_eiger_detector.trigger() - await _simulate_one_trigger(1) - await status + await mock_eiger_detector.trigger() await mock_eiger_detector.read() - status = mock_eiger_detector.trigger() - await _simulate_one_trigger(2) - await status + await mock_eiger_detector.trigger() await mock_eiger_detector.read() await mock_eiger_detector.unstage() @@ -581,61 +560,38 @@ async def test_eiger_detector_with_RE( RE: RunEngine, tiled_client: Container, mock_eiger_detector: EigerDetector ) -> None: set_mock_value(mock_eiger_detector.fileio.num_captured, 0) - acquire_period = 0.01 - - def _count_plan( - dets: Sequence[EigerDetector], - num: int = 1, - num_images: int = 1, - sequence_id: int = 1, - ) -> MsgGenerator[str]: - yield from bps.stage_all(*dets) - - for det in dets: - set_mock_value(det.fileio.num_captured, 0) - - yield from bps.open_run() - - for _ in range(num): - read_values = {} - for det in dets: - read_values[det] = yield from bps.rd(det.fileio.num_captured) - - for det in dets: - yield from bps.trigger(det, wait=False, group="wait_for_trigger") - - yield from bps.sleep(acquire_period) + async def _write_file(value: bool, wait: bool) -> None: + if value: + num_images = await mock_eiger_detector.driver.num_images.get_value() + sequence_id = await mock_eiger_detector.fileio.sequence_id.get_value() + await asyncio.sleep( + await mock_eiger_detector.driver.acquire_period.get_value() + ) write_eiger_hdf5_file( num_triggers=1, num_images=num_images, sequence_id=sequence_id, name="test_eiger", ) - - for det in dets: - num_images = yield from bps.rd(det.driver.num_images) - set_mock_value(det.fileio.num_captured, read_values[det] + num_images) - - yield from bps.wait(group="wait_for_trigger") - yield from bps.create() - - for det in dets: - yield from bps.read(det) - - yield from bps.save() - - yield from bps.close_run() - yield from bps.unstage_all(*dets) + num_captured = await mock_eiger_detector.fileio.num_captured.get_value() + set_mock_value( + mock_eiger_detector.fileio.num_captured, num_captured + num_images + ) tiled_writer = TiledWriter(tiled_client) RE.subscribe(tiled_writer) + callback_on_mock_put(mock_eiger_detector.driver.acquire, _write_file) set_mock_value(mock_eiger_detector.fileio.sequence_id, 1) set_mock_value(mock_eiger_detector.driver.num_images, 1) - uid = RE(_count_plan([mock_eiger_detector])) + set_mock_value(mock_eiger_detector.fileio.num_captured, 0) + set_mock_value(mock_eiger_detector.driver.acquire_period, 0.001) + uid = RE(bp.count([mock_eiger_detector])) assert uid is not None - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_image"].shape == ( + assert tiled_client.values().last()["streams"]["primary"][ + "test_eiger_image" + ].shape == ( 1, 2048, 2048, @@ -728,9 +684,13 @@ def _count_plan( set_mock_value(mock_eiger_detector.fileio.sequence_id, 2) set_mock_value(mock_eiger_detector.driver.num_images, 1) - uid = RE(_count_plan([mock_eiger_detector], num=10, num_images=1, sequence_id=2)) + set_mock_value(mock_eiger_detector.fileio.num_captured, 0) + set_mock_value(mock_eiger_detector.driver.acquire_period, 0.001) + uid = RE(bp.count([mock_eiger_detector], num=10)) assert uid is not None - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_image"].shape == ( + assert tiled_client.values().last()["streams"]["primary"][ + "test_eiger_image" + ].shape == ( 10, 2048, 2048, From 402570b30284f6c207d26079f5ec4c35532d35a0 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Tue, 15 Jul 2025 15:26:51 -0400 Subject: [PATCH 22/46] NDFileIO back to inheriting from NDArrayBaseIO --- src/cditools/eiger_async.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index 9b604fc2..11a67b1a 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -34,7 +34,7 @@ ADImageMode, ADWriter, AreaDetector, - NDPluginBaseIO, + NDArrayBaseIO, ) from ophyd_async.epics.signal import PvSuffix @@ -43,7 +43,7 @@ # TODO: Port to ophyd-async (https://github.com/bluesky/ophyd-async/issues/961) # ============================================================================== -class NDFileIO(NDPluginBaseIO): +class NDFileIO(NDArrayBaseIO): file_path: A[SignalRW[str], PvSuffix.rbv("FilePath")] file_name: A[SignalRW[str], PvSuffix.rbv("FileName")] file_path_exists: A[SignalR[bool], PvSuffix("FilePathExists_RBV")] @@ -58,8 +58,6 @@ class NDFileIO(NDPluginBaseIO): array_size0: A[SignalR[int], PvSuffix("ArraySize0")] array_size1: A[SignalR[int], PvSuffix("ArraySize1")] create_directory: A[SignalRW[int], PvSuffix("CreateDirectory")] - - # ============================================================================== From 7f7e53f769cb17aac14fc67427a3c12e01442793 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Tue, 15 Jul 2025 16:57:49 -0400 Subject: [PATCH 23/46] Add nimgs_per_file with correct PV string; Add comment about num_captured not used in ADEiger --- src/cditools/eiger_async.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index 11a67b1a..442c2745 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -35,6 +35,7 @@ ADWriter, AreaDetector, NDArrayBaseIO, + NDPluginBaseIO, ) from ophyd_async.epics.signal import PvSuffix @@ -295,15 +296,16 @@ class EigerFileIO(NDFileIO): fw_free: A[SignalR[float], PvSuffix("FWFree_RBV")] fw_auto_remove: A[SignalRW[bool], PvSuffix.rbv("FWAutoRemove")] fw_clear: A[SignalRW[float], PvSuffix("FWClear")] + fw_nimgs_per_file: A[SignalRW[int], PvSuffix.rbv("FWNImagesPerFile")] class Eiger2FileIO(EigerFileIO): """FileWriter interface for the Eiger2 detector.""" fw_hdf5_format: A[SignalRW[EigerHDF5Format], PvSuffix.rbv("FWHDF5Format")] - fw_nimgs_per_file: A[SignalRW[int], PvSuffix.rbv("FWNImgsPerFile")] +# TODO: DO NOT WAIT FOR NUM_CAPTURED SINCE IT DOES NOT GET UPDATED, USE NUM_IMAGES_COUNTER INSTEAD class EigerWriter(ADWriter[EigerFileIO]): """Eiger-specific file writer using the built-in FileWriter interface.""" @@ -344,6 +346,7 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK # Configure the Eiger FileWriter await asyncio.gather( self.fileio.file_path.set(self._file_info.directory_path.as_posix()), + self.fileio.create_directory.set(-3), self.fileio.fw_name_pattern.set(name_pattern), self.fileio.fw_enable.set(True), self.fileio.save_files.set(True), @@ -351,12 +354,12 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK if isinstance(self.fileio, Eiger2FileIO): await self.fileio.fw_hdf5_format.set(EigerHDF5Format.LEGACY) - # Force the number of images per file to a large number to simplify the logic - await self.fileio.fw_nimgs_per_file.set(self._min_num_images_per_file) - logger.warning( - "Setting fw_nimgs_per_file to %d to force writing to a single HDF5 file", - self._min_num_images_per_file, - ) + # Force the number of images per file to a large number to simplify the logic + await self.fileio.fw_nimgs_per_file.set(self._min_num_images_per_file) + logger.warning( + "Setting fw_nimgs_per_file to %d to force writing to a single HDF5 file", + self._min_num_images_per_file, + ) detector_shape, np_dtype = await asyncio.gather( self._dataset_describer.shape(), From 218862d9b391ddb0050e84753ed430e5a964e332 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Wed, 16 Jul 2025 10:54:05 -0400 Subject: [PATCH 24/46] Merged EigerFileIO into EigerDriverIO to access driver PVs in EigerWriter --- src/cditools/eiger_async.py | 93 ++++++++++++++++++++++--------------- 1 file changed, 56 insertions(+), 37 deletions(-) diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index 442c2745..5fb8d23c 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -5,7 +5,7 @@ from __future__ import annotations import asyncio -from collections.abc import AsyncIterator, Sequence +from collections.abc import AsyncGenerator, AsyncIterator, Sequence from logging import getLogger from pathlib import Path from typing import Annotated as A @@ -25,6 +25,7 @@ SignalRW, StrictEnum, TriggerInfo, + observe_value, ) from ophyd_async.epics.adcore import ( ADBaseController, @@ -59,6 +60,8 @@ class NDFileIO(NDArrayBaseIO): array_size0: A[SignalR[int], PvSuffix("ArraySize0")] array_size1: A[SignalR[int], PvSuffix("ArraySize1")] create_directory: A[SignalRW[int], PvSuffix("CreateDirectory")] + + # ============================================================================== @@ -147,7 +150,7 @@ class EigerStreamHdrDetail(StrictEnum): NONE = "None" -class EigerDriverIO(ADBaseIO): +class EigerDriverIO(ADBaseIO, NDFileIO): """Defines the full specifics of the Eiger driver. See https://areadetector.github.io/areaDetector/ADEiger/eiger.html#implementation-of-standard-driver-parameters @@ -251,6 +254,21 @@ class EigerDriverIO(ADBaseIO): wavelength_eps: A[SignalRW[float], PvSuffix.rbv("WavelengthEps")] energy_eps: A[SignalRW[float], PvSuffix.rbv("EnergyEps")] + # FileWriter Interface + fw_enable: A[SignalRW[bool], PvSuffix.rbv("FWEnable")] + fw_state: A[SignalR[str], PvSuffix("FWState_RBV")] + fw_compression: A[SignalRW[bool], PvSuffix.rbv("FWCompression")] + fw_name_pattern: A[SignalRW[str], PvSuffix.rbv("FWNamePattern")] + sequence_id: A[SignalR[int], PvSuffix("SequenceId")] + save_files: A[SignalRW[bool], PvSuffix.rbv("SaveFiles")] + file_owner: A[SignalRW[str], PvSuffix.rbv("FileOwner")] + file_owner_grp: A[SignalRW[str], PvSuffix.rbv("FileOwnerGrp")] + file_perms: A[SignalRW[float], PvSuffix.rbv("FilePerms")] + fw_free: A[SignalR[float], PvSuffix("FWFree_RBV")] + fw_auto_remove: A[SignalRW[bool], PvSuffix.rbv("FWAutoRemove")] + fw_clear: A[SignalRW[float], PvSuffix("FWClear")] + fw_nimgs_per_file: A[SignalRW[int], PvSuffix.rbv("FWNImagesPerFile")] + class Eiger2DriverIO(EigerDriverIO): """Eiger2 driver interface.""" @@ -280,33 +298,11 @@ class Eiger2DriverIO(EigerDriverIO): stream_hdr_appendix: A[SignalRW[str], PvSuffix.rbv("StreamHdrAppendix")] stream_img_appendix: A[SignalRW[str], PvSuffix.rbv("StreamImgAppendix")] - -class EigerFileIO(NDFileIO): - """FileWriter interface for the Eiger detector.""" - - fw_enable: A[SignalRW[bool], PvSuffix.rbv("FWEnable")] - fw_state: A[SignalR[str], PvSuffix("FWState_RBV")] - fw_compression: A[SignalRW[bool], PvSuffix.rbv("FWCompression")] - fw_name_pattern: A[SignalRW[str], PvSuffix.rbv("FWNamePattern")] - sequence_id: A[SignalR[int], PvSuffix("SequenceId")] - save_files: A[SignalRW[bool], PvSuffix.rbv("SaveFiles")] - file_owner: A[SignalRW[str], PvSuffix.rbv("FileOwner")] - file_owner_grp: A[SignalRW[str], PvSuffix.rbv("FileOwnerGrp")] - file_perms: A[SignalRW[float], PvSuffix.rbv("FilePerms")] - fw_free: A[SignalR[float], PvSuffix("FWFree_RBV")] - fw_auto_remove: A[SignalRW[bool], PvSuffix.rbv("FWAutoRemove")] - fw_clear: A[SignalRW[float], PvSuffix("FWClear")] - fw_nimgs_per_file: A[SignalRW[int], PvSuffix.rbv("FWNImagesPerFile")] - - -class Eiger2FileIO(EigerFileIO): - """FileWriter interface for the Eiger2 detector.""" - + # FileWriter Interface fw_hdf5_format: A[SignalRW[EigerHDF5Format], PvSuffix.rbv("FWHDF5Format")] -# TODO: DO NOT WAIT FOR NUM_CAPTURED SINCE IT DOES NOT GET UPDATED, USE NUM_IMAGES_COUNTER INSTEAD -class EigerWriter(ADWriter[EigerFileIO]): +class EigerWriter(ADWriter[EigerDriverIO]): """Eiger-specific file writer using the built-in FileWriter interface.""" default_suffix: str = "cam1:" @@ -315,7 +311,7 @@ class EigerWriter(ADWriter[EigerFileIO]): def __init__( self, - fileio: EigerFileIO, + fileio: EigerDriverIO, path_provider: PathProvider, dataset_describer: ADBaseDatasetDescriber, plugins: dict[str, NDPluginBaseIO] | None = None, @@ -352,7 +348,7 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK self.fileio.save_files.set(True), ) - if isinstance(self.fileio, Eiger2FileIO): + if isinstance(self.fileio, Eiger2DriverIO): await self.fileio.fw_hdf5_format.set(EigerHDF5Format.LEGACY) # Force the number of images per file to a large number to simplify the logic await self.fileio.fw_nimgs_per_file.set(self._min_num_images_per_file) @@ -500,6 +496,20 @@ async def collect_stream_docs( for doc in self._composer.stream_data(indices_written): yield "stream_datum", doc + async def observe_indices_written( + self, timeout: float + ) -> AsyncGenerator[int, None]: + async for num_captured in observe_value( + self.fileio.num_images_counter, timeout + ): + yield num_captured // self._exposures_per_event + + async def get_indices_written(self) -> int: + return ( + await self.fileio.num_images_counter.get_value() + // self._exposures_per_event + ) + async def close(self) -> None: """Clean up file writing after acquisition and validate files exist.""" # Disable file writer and reset number of images per file @@ -577,13 +587,23 @@ def __init__( ): driver = EigerDriverIO(prefix + driver_suffix) controller = EigerController(driver) - writer = writer_cls.with_io( - prefix, - path_provider, - dataset_source=driver, - fileio_suffix=fileio_suffix, - plugins=plugins, - ) + if issubclass(writer_cls, EigerWriter): + dataset_describer = ADBaseDatasetDescriber(driver) + # EigerWriter takes the driver as the fileio, since it relies on driver PVs + writer = writer_cls( + driver, + path_provider, + dataset_describer=dataset_describer, + plugins=plugins, + ) + else: + writer = writer_cls.with_io( + prefix, + path_provider, + dataset_source=driver, + fileio_suffix=fileio_suffix, + plugins=plugins, + ) super().__init__( controller=controller, @@ -595,7 +615,6 @@ def __init__( # TODO: Fix typing in base class self.driver: EigerDriverIO = cast(EigerDriverIO, self.driver) - self.fileio: EigerFileIO = cast(EigerFileIO, self.fileio) @AsyncStatus.wrap async def stage(self) -> None: @@ -605,7 +624,7 @@ async def stage(self) -> None: Otherwise, set the data source to STREAM. """ await super().stage() - if isinstance(self.fileio, EigerFileIO): + if isinstance(self.fileio, EigerDriverIO): await self.driver.data_source.set(EigerDataSource.FILE_WRITER) else: await self.driver.data_source.set(EigerDataSource.STREAM) From b51e340295351c64f6c4a91ea54297a592d19b63 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Wed, 16 Jul 2025 11:57:54 -0400 Subject: [PATCH 25/46] Use array_counter instead of num_images_counter because we can set it to 0 at the start --- pixi.lock | 5 +++-- src/cditools/eiger_async.py | 34 +++++++++++++--------------------- 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/pixi.lock b/pixi.lock index c0ba3879..81d6cde3 100644 --- a/pixi.lock +++ b/pixi.lock @@ -997,8 +997,8 @@ packages: requires_python: '>=3.8' - pypi: . name: cditools - version: 0.1.dev55+ge4c43d8.d20250715 - sha256: a9151703cfcc98e3ec8b42f6f48a3d0b846f2cbe302adfad988cb1c3e94ab350 + version: 0.1.dev63+gc62d9d2 + sha256: 263040a2b9c717fae683c72a50ab6bc1b125d49e1b6d51cc5587063d9544f105 requires_dist: - ophyd - ophyd-async>=0.10.0a4 @@ -1020,6 +1020,7 @@ packages: - ruff ; extra == 'dev' - aioca ; extra == 'dev' - tiled[minimal-client,server] ; extra == 'dev' + - pyright ; extra == 'dev' - sphinx>=7.0 ; extra == 'docs' - myst-parser>=0.13 ; extra == 'docs' - sphinx-copybutton ; extra == 'docs' diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index 5fb8d23c..1625942f 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -342,14 +342,23 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK # Configure the Eiger FileWriter await asyncio.gather( self.fileio.file_path.set(self._file_info.directory_path.as_posix()), - self.fileio.create_directory.set(-3), + self.fileio.create_directory.set(self._file_info.create_dir_depth), self.fileio.fw_name_pattern.set(name_pattern), self.fileio.fw_enable.set(True), self.fileio.save_files.set(True), + self.fileio.data_source.set(EigerDataSource.FILE_WRITER), + self.fileio.num_capture.set(0), + # Use array_counter to track the total number of images written + self.fileio.array_counter.set(0), ) + if not await self.fileio.file_path_exists.get_value(): + msg = f"File path {self._file_info.directory_path} does not exist" + raise FileNotFoundError(msg) + if isinstance(self.fileio, Eiger2DriverIO): await self.fileio.fw_hdf5_format.set(EigerHDF5Format.LEGACY) + # Force the number of images per file to a large number to simplify the logic await self.fileio.fw_nimgs_per_file.set(self._min_num_images_per_file) logger.warning( @@ -476,11 +485,10 @@ def _master_file_path(self) -> Path | None: ) async def collect_stream_docs( - self, _name: str, indices_written: int + self, name: str, indices_written: int ) -> AsyncIterator[StreamAsset]: """Generate stream documents for the written HDF5 files.""" if indices_written: - logger.warning(f"COLLECTING STREAM DOCS: {indices_written}") if not self._composer: if self._master_file_path is None: raise ValueError("Master file path is not set") @@ -500,13 +508,13 @@ async def observe_indices_written( self, timeout: float ) -> AsyncGenerator[int, None]: async for num_captured in observe_value( - self.fileio.num_images_counter, timeout + self.fileio.array_counter, timeout ): yield num_captured // self._exposures_per_event async def get_indices_written(self) -> int: return ( - await self.fileio.num_images_counter.get_value() + await self.fileio.array_counter.get_value() // self._exposures_per_event ) @@ -612,19 +620,3 @@ def __init__( name=name, config_sigs=config_sigs, ) - - # TODO: Fix typing in base class - self.driver: EigerDriverIO = cast(EigerDriverIO, self.driver) - - @AsyncStatus.wrap - async def stage(self) -> None: - """Prepare the eiger detector for acquisition. - - If the writer is an EigerWriter, set the data source to FILE_WRITER. - Otherwise, set the data source to STREAM. - """ - await super().stage() - if isinstance(self.fileio, EigerDriverIO): - await self.driver.data_source.set(EigerDataSource.FILE_WRITER) - else: - await self.driver.data_source.set(EigerDataSource.STREAM) From 665951a19fc1c80fe500de34d737997164b60ecb Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Wed, 16 Jul 2025 15:10:38 -0400 Subject: [PATCH 26/46] Remove the disable part of close --- src/cditools/eiger_async.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index 1625942f..3e8fdfaf 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -520,12 +520,8 @@ async def get_indices_written(self) -> int: async def close(self) -> None: """Clean up file writing after acquisition and validate files exist.""" - # Disable file writer and reset number of images per file - await asyncio.gather( - self.fileio.fw_enable.set(False), - self.fileio.save_files.set(False), - ) + # Check that the master file was written if self._master_file_path is not None and not self._master_file_path.exists(): logger.warning("Master file was not written: %s", self._master_file_path) From f538158b033c2627436b954c96cb2d95554d01c4 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Wed, 16 Jul 2025 15:50:25 -0400 Subject: [PATCH 27/46] Update sequence_id offset --- src/cditools/eiger_async.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index 3e8fdfaf..0363ef72 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -334,7 +334,7 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK """Setup file writing for acquisition.""" # Get file path info from path provider self._file_info = self._path_provider() - self._current_sequence_id = await self.fileio.sequence_id.get_value() + self._current_sequence_id = await self.fileio.sequence_id.get_value() + 1 # Set the name pattern with $id replacement similar to original name_pattern = f"{self._file_info.filename}_$id" From 50eac1bdf6ede1c99da3fb90614d14128c2c79b0 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Wed, 16 Jul 2025 16:48:02 -0400 Subject: [PATCH 28/46] Updated tests --- tests/test_eiger_async.py | 111 ++++++++++++++++++-------------------- 1 file changed, 51 insertions(+), 60 deletions(-) diff --git a/tests/test_eiger_async.py b/tests/test_eiger_async.py index 55047090..4cdb59d4 100644 --- a/tests/test_eiger_async.py +++ b/tests/test_eiger_async.py @@ -4,6 +4,7 @@ from __future__ import annotations +import os import asyncio import shutil from collections.abc import Generator @@ -35,7 +36,6 @@ EigerDataSource, EigerDetector, EigerDriverIO, - EigerFileIO, EigerTriggerMode, EigerWriter, ) @@ -44,10 +44,6 @@ def write_eiger_hdf5_file( num_triggers: int, num_images: int, sequence_id: int, name: str = "test_eiger" ): - data_path = Path("/tmp/test_data/") - if not data_path.exists(): - data_path.mkdir(parents=True) - with h5py.File(f"/tmp/test_data/{name}_{sequence_id}_data_000001.h5", "w") as f: f.create_dataset( "data_000001", @@ -98,11 +94,15 @@ def write_eiger_hdf5_file( @pytest.fixture def mock_eiger_detector(RE: RunEngine) -> Generator[EigerDetector, None, None]: + data_path = Path("/tmp/test_data/") + if not data_path.exists(): + data_path.mkdir(parents=True) path_provider = StaticPathProvider( StaticFilenameProvider("test_eiger"), directory_path="/tmp/test_data" ) with init_devices(mock=True): detector = EigerDetector("MOCK:EIGER:", path_provider, name="test_eiger") + set_mock_value(detector.fileio.file_path_exists, True) set_mock_value(detector.driver.array_size_x, 2048) set_mock_value(detector.driver.array_size_y, 2048) set_mock_value(detector.driver.data_type, "UInt16") @@ -121,6 +121,7 @@ def mock_eiger_driver(RE: RunEngine) -> EigerDriverIO: driver = EigerDriverIO("MOCK:EIGER:cam1:") # Set up some default mock values + set_mock_value(driver.file_path_exists, True) set_mock_value(driver.array_size_x, 2048) set_mock_value(driver.array_size_y, 2048) set_mock_value(driver.data_type, "UInt16") @@ -128,14 +129,6 @@ def mock_eiger_driver(RE: RunEngine) -> EigerDriverIO: return driver -@pytest.fixture -def mock_eiger_fileio(RE: RunEngine) -> EigerFileIO: - with init_devices(mock=True): - fileio = EigerFileIO("MOCK:EIGER:cam1:") - - return fileio - - @pytest.fixture def mock_path_provider() -> PathProvider: """Create a mock path provider for testing.""" @@ -147,12 +140,17 @@ def mock_path_provider() -> PathProvider: @pytest.fixture def eiger_writer( mock_eiger_driver: EigerDriverIO, - mock_eiger_fileio: EigerFileIO, mock_path_provider: PathProvider, -) -> EigerWriter: +) -> Generator[EigerWriter, None, None]: """Create an EigerWriter instance for testing.""" + data_path = Path("/tmp/test_data/") + if not data_path.exists(): + data_path.mkdir(parents=True) + assert os.path.exists("/tmp/test_data") dataset_describer = ADBaseDatasetDescriber(mock_eiger_driver) - return EigerWriter(mock_eiger_fileio, mock_path_provider, dataset_describer) + yield EigerWriter(mock_eiger_driver, mock_path_provider, dataset_describer) + if data_path.exists(): + shutil.rmtree(data_path) @pytest.fixture @@ -163,11 +161,11 @@ def eiger_controller(mock_eiger_driver: EigerDriverIO) -> EigerController: @pytest.mark.asyncio async def test_eiger_writer_initialization( eiger_writer: EigerWriter, - mock_eiger_fileio: EigerFileIO, + mock_eiger_driver: EigerDriverIO, mock_path_provider: PathProvider, ): """Test that EigerWriter initializes correctly.""" - assert eiger_writer.fileio is mock_eiger_fileio + assert eiger_writer.fileio is mock_eiger_driver assert eiger_writer._path_provider is mock_path_provider assert eiger_writer._dataset_describer is not None assert eiger_writer._file_info is None @@ -179,7 +177,6 @@ async def test_eiger_writer_initialization( async def test_eiger_writer_open( eiger_writer: EigerWriter, mock_eiger_driver: EigerDriverIO, - mock_eiger_fileio: EigerFileIO, ) -> None: """Test the open method configures the detector correctly.""" array_size_x, array_size_y, data_type = await asyncio.gather( @@ -189,13 +186,13 @@ async def test_eiger_writer_open( ) # Case 1: 1 image, 1 trigger - set_mock_value(mock_eiger_fileio.sequence_id, 1) + set_mock_value(mock_eiger_driver.sequence_id, 1) set_mock_value(mock_eiger_driver.num_images, 1) set_mock_value(mock_eiger_driver.num_triggers, 1) description = await eiger_writer.open(name="test_eiger", exposures_per_event=1) - assert await mock_eiger_fileio.fw_enable.get_value() is True - assert await mock_eiger_fileio.save_files.get_value() is True + assert await mock_eiger_driver.fw_enable.get_value() is True + assert await mock_eiger_driver.save_files.get_value() is True assert description.keys() == { "test_eiger_y_pixel_size", "test_eiger_x_pixel_size", @@ -215,7 +212,7 @@ async def test_eiger_writer_open( # Case 2: 4 images per file, 11 images, 2 triggers # Expect 6 files, the first 5 will have 4 images, the last will have 2 - set_mock_value(mock_eiger_fileio.sequence_id, 2) + set_mock_value(mock_eiger_driver.sequence_id, 2) set_mock_value(mock_eiger_driver.num_images, 11) set_mock_value(mock_eiger_driver.num_triggers, 2) description = await eiger_writer.open( @@ -248,55 +245,54 @@ async def test_eiger_writer_open( async def test_eiger_writer_get_indices_written( eiger_writer: EigerWriter, mock_eiger_driver: EigerDriverIO, - mock_eiger_fileio: EigerFileIO, ): """Test getting the number of indices written.""" - set_mock_value(mock_eiger_fileio.sequence_id, 1) + set_mock_value(mock_eiger_driver.sequence_id, 1) # Case 1: 1 image, 1 trigger set_mock_value(mock_eiger_driver.num_triggers, 1) set_mock_value(mock_eiger_driver.num_images, 1) - set_mock_value(mock_eiger_fileio.num_captured, 0) + set_mock_value(mock_eiger_driver.array_counter, 0) await eiger_writer.open( name="test_eiger", exposures_per_event=await mock_eiger_driver.num_images.get_value(), ) assert await eiger_writer.get_indices_written() == 0 - set_mock_value(mock_eiger_fileio.num_captured, 1) + set_mock_value(mock_eiger_driver.array_counter, 1) assert await eiger_writer.get_indices_written() == 1 # Case 2: 1 image, 5 triggers set_mock_value(mock_eiger_driver.num_triggers, 5) set_mock_value(mock_eiger_driver.num_images, 1) - set_mock_value(mock_eiger_fileio.num_captured, 0) + set_mock_value(mock_eiger_driver.array_counter, 0) await eiger_writer.open( name="test_eiger", exposures_per_event=await mock_eiger_driver.num_images.get_value(), ) assert await eiger_writer.get_indices_written() == 0 - set_mock_value(mock_eiger_fileio.num_captured, 1) + set_mock_value(mock_eiger_driver.array_counter, 1) assert await eiger_writer.get_indices_written() == 1 - set_mock_value(mock_eiger_fileio.num_captured, 3) + set_mock_value(mock_eiger_driver.array_counter, 3) assert await eiger_writer.get_indices_written() == 3 - set_mock_value(mock_eiger_fileio.num_captured, 5) + set_mock_value(mock_eiger_driver.array_counter, 5) assert await eiger_writer.get_indices_written() == 5 # Case 3: 5 images, 2 triggers set_mock_value(mock_eiger_driver.num_triggers, 2) set_mock_value(mock_eiger_driver.num_images, 5) - set_mock_value(mock_eiger_fileio.num_captured, 0) + set_mock_value(mock_eiger_driver.array_counter, 0) await eiger_writer.open( name="test_eiger", exposures_per_event=await mock_eiger_driver.num_images.get_value(), ) assert await eiger_writer.get_indices_written() == 0 - set_mock_value(mock_eiger_fileio.num_captured, 4) + set_mock_value(mock_eiger_driver.array_counter, 4) assert await eiger_writer.get_indices_written() == 0 - set_mock_value(mock_eiger_fileio.num_captured, 5) + set_mock_value(mock_eiger_driver.array_counter, 5) assert await eiger_writer.get_indices_written() == 1 - set_mock_value(mock_eiger_fileio.num_captured, 9) + set_mock_value(mock_eiger_driver.array_counter, 9) assert await eiger_writer.get_indices_written() == 1 - set_mock_value(mock_eiger_fileio.num_captured, 10) + set_mock_value(mock_eiger_driver.array_counter, 10) assert await eiger_writer.get_indices_written() == 2 @@ -304,23 +300,22 @@ async def test_eiger_writer_get_indices_written( async def test_eiger_writer_observe_indices_written( eiger_writer: EigerWriter, mock_eiger_driver: EigerDriverIO, - mock_eiger_fileio: EigerFileIO, ) -> None: """Test observing indices as they are written.""" - set_mock_value(mock_eiger_fileio.sequence_id, 1) + set_mock_value(mock_eiger_driver.sequence_id, 1) async def _simulate_writing_indices( num_images: int, num_triggers: int, acquire_time: float = 0.01 ) -> list[int]: # Create an async generator to track yielded indices observed_indices = [] - set_mock_value(mock_eiger_fileio.num_captured, 0) + set_mock_value(mock_eiger_driver.array_counter, 0) async def _simulate_acquisition(): """Simulate the detector writing images by incrementing sequence_id.""" for i in range(1, num_images * num_triggers + 1): await asyncio.sleep(acquire_time) - set_mock_value(mock_eiger_fileio.num_captured, i) + set_mock_value(mock_eiger_driver.array_counter, i) async def _complete(): """Helper function to collect observed indices.""" @@ -379,7 +374,6 @@ async def _complete(): async def test_eiger_writer_collect_stream_docs( eiger_writer: EigerWriter, mock_eiger_driver: EigerDriverIO, - mock_eiger_fileio: EigerFileIO, ) -> None: """Test collecting stream documents.""" @@ -388,7 +382,7 @@ async def collect_docs(num_triggers: int): data_docs = [] for i in range(1, num_triggers + 1): async for doc_type, doc in eiger_writer.collect_stream_docs( - _name="", indices_written=i + name="", indices_written=i ): if doc_type == "stream_resource": resource_docs.append(doc) @@ -396,7 +390,7 @@ async def collect_docs(num_triggers: int): data_docs.append(doc) return resource_docs, data_docs - set_mock_value(mock_eiger_fileio.sequence_id, 1) + set_mock_value(mock_eiger_driver.sequence_id, 1) set_mock_value(mock_eiger_driver.num_images, 1) set_mock_value(mock_eiger_driver.num_triggers, 1) await eiger_writer.open(name="test_eiger", exposures_per_event=1) @@ -413,7 +407,7 @@ async def collect_docs(num_triggers: int): await eiger_writer.close() set_mock_value(mock_eiger_driver.num_triggers, 3) - set_mock_value(mock_eiger_fileio.sequence_id, 2) + set_mock_value(mock_eiger_driver.sequence_id, 2) await eiger_writer.open(name="test_eiger", exposures_per_event=1) resource_docs, data_docs = await collect_docs( num_triggers=await mock_eiger_driver.num_triggers.get_value() @@ -430,22 +424,19 @@ async def collect_docs(num_triggers: int): async def test_eiger_writer_close( eiger_writer: EigerWriter, mock_eiger_driver: EigerDriverIO, - mock_eiger_fileio: EigerFileIO, ) -> None: """Test closing the writer.""" # Verify the writing was enabled - set_mock_value(mock_eiger_fileio.sequence_id, 1) + set_mock_value(mock_eiger_driver.sequence_id, 1) set_mock_value(mock_eiger_driver.num_images, 1) set_mock_value(mock_eiger_driver.num_triggers, 1) await eiger_writer.open(name="test_eiger", exposures_per_event=1) - assert await mock_eiger_fileio.fw_enable.get_value() is True - assert await mock_eiger_fileio.save_files.get_value() is True + assert await mock_eiger_driver.fw_enable.get_value() is True + assert await mock_eiger_driver.save_files.get_value() is True # Verify the writing was disabled await eiger_writer.close() - assert await mock_eiger_fileio.fw_enable.get_value() is False - assert await mock_eiger_fileio.save_files.get_value() is False assert eiger_writer._composer is None assert eiger_writer._current_sequence_id is None assert eiger_writer._file_info is None @@ -515,12 +506,12 @@ async def test_eiger_detector(mock_eiger_detector: EigerDetector) -> None: set_mock_value(mock_eiger_detector.driver.num_images, 1) set_mock_value(mock_eiger_detector.driver.num_triggers, 2) set_mock_value(mock_eiger_detector.driver.acquire_period, 0.001) - set_mock_value(mock_eiger_detector.fileio.num_captured, 0) + set_mock_value(mock_eiger_detector.fileio.array_counter, 0) async def _simulate_one_trigger(value: bool, wait: bool) -> None: await asyncio.sleep(await mock_eiger_detector.driver.acquire_period.get_value()) - num_captured = await mock_eiger_detector.fileio.num_captured.get_value() - set_mock_value(mock_eiger_detector.fileio.num_captured, num_captured + 1) + array_counter = await mock_eiger_detector.fileio.array_counter.get_value() + set_mock_value(mock_eiger_detector.fileio.array_counter, array_counter + 1) callback_on_mock_put(mock_eiger_detector.driver.acquire, _simulate_one_trigger) @@ -529,17 +520,17 @@ async def _simulate_one_trigger(value: bool, wait: bool) -> None: # Case 1 - Step Scan: stage, trigger, read, trigger, read, unstage await mock_eiger_detector.stage() + await mock_eiger_detector.trigger() assert ( await mock_eiger_detector.driver.data_source.get_value() == EigerDataSource.FILE_WRITER ) - await mock_eiger_detector.trigger() await mock_eiger_detector.read() await mock_eiger_detector.trigger() await mock_eiger_detector.read() await mock_eiger_detector.unstage() - set_mock_value(mock_eiger_detector.fileio.num_captured, 0) + set_mock_value(mock_eiger_detector.fileio.array_counter, 0) # Case 2 - Fly Scan: prepare, kickoff, complete await mock_eiger_detector.prepare( TriggerInfo( @@ -559,7 +550,7 @@ async def _simulate_one_trigger(value: bool, wait: bool) -> None: async def test_eiger_detector_with_RE( RE: RunEngine, tiled_client: Container, mock_eiger_detector: EigerDetector ) -> None: - set_mock_value(mock_eiger_detector.fileio.num_captured, 0) + set_mock_value(mock_eiger_detector.fileio.array_counter, 0) async def _write_file(value: bool, wait: bool) -> None: if value: @@ -574,9 +565,9 @@ async def _write_file(value: bool, wait: bool) -> None: sequence_id=sequence_id, name="test_eiger", ) - num_captured = await mock_eiger_detector.fileio.num_captured.get_value() + array_counter = await mock_eiger_detector.fileio.array_counter.get_value() set_mock_value( - mock_eiger_detector.fileio.num_captured, num_captured + num_images + mock_eiger_detector.fileio.array_counter, array_counter + num_images ) tiled_writer = TiledWriter(tiled_client) @@ -585,7 +576,7 @@ async def _write_file(value: bool, wait: bool) -> None: set_mock_value(mock_eiger_detector.fileio.sequence_id, 1) set_mock_value(mock_eiger_detector.driver.num_images, 1) - set_mock_value(mock_eiger_detector.fileio.num_captured, 0) + set_mock_value(mock_eiger_detector.fileio.array_counter, 0) set_mock_value(mock_eiger_detector.driver.acquire_period, 0.001) uid = RE(bp.count([mock_eiger_detector])) assert uid is not None @@ -684,7 +675,7 @@ async def _write_file(value: bool, wait: bool) -> None: set_mock_value(mock_eiger_detector.fileio.sequence_id, 2) set_mock_value(mock_eiger_detector.driver.num_images, 1) - set_mock_value(mock_eiger_detector.fileio.num_captured, 0) + set_mock_value(mock_eiger_detector.fileio.array_counter, 0) set_mock_value(mock_eiger_detector.driver.acquire_period, 0.001) uid = RE(bp.count([mock_eiger_detector], num=10)) assert uid is not None From 637b64f2db1b4a47224d4899e965acebc6f91ea8 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Wed, 16 Jul 2025 16:50:54 -0400 Subject: [PATCH 29/46] Fix sequence_id offset in tests --- tests/test_eiger_async.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/test_eiger_async.py b/tests/test_eiger_async.py index 4cdb59d4..f389dc92 100644 --- a/tests/test_eiger_async.py +++ b/tests/test_eiger_async.py @@ -44,6 +44,7 @@ def write_eiger_hdf5_file( num_triggers: int, num_images: int, sequence_id: int, name: str = "test_eiger" ): + sequence_id += 1 with h5py.File(f"/tmp/test_data/{name}_{sequence_id}_data_000001.h5", "w") as f: f.create_dataset( "data_000001", @@ -186,7 +187,7 @@ async def test_eiger_writer_open( ) # Case 1: 1 image, 1 trigger - set_mock_value(mock_eiger_driver.sequence_id, 1) + set_mock_value(mock_eiger_driver.sequence_id, 0) set_mock_value(mock_eiger_driver.num_images, 1) set_mock_value(mock_eiger_driver.num_triggers, 1) @@ -212,7 +213,7 @@ async def test_eiger_writer_open( # Case 2: 4 images per file, 11 images, 2 triggers # Expect 6 files, the first 5 will have 4 images, the last will have 2 - set_mock_value(mock_eiger_driver.sequence_id, 2) + set_mock_value(mock_eiger_driver.sequence_id, 1) set_mock_value(mock_eiger_driver.num_images, 11) set_mock_value(mock_eiger_driver.num_triggers, 2) description = await eiger_writer.open( @@ -390,7 +391,7 @@ async def collect_docs(num_triggers: int): data_docs.append(doc) return resource_docs, data_docs - set_mock_value(mock_eiger_driver.sequence_id, 1) + set_mock_value(mock_eiger_driver.sequence_id, 0) set_mock_value(mock_eiger_driver.num_images, 1) set_mock_value(mock_eiger_driver.num_triggers, 1) await eiger_writer.open(name="test_eiger", exposures_per_event=1) @@ -407,7 +408,7 @@ async def collect_docs(num_triggers: int): await eiger_writer.close() set_mock_value(mock_eiger_driver.num_triggers, 3) - set_mock_value(mock_eiger_driver.sequence_id, 2) + set_mock_value(mock_eiger_driver.sequence_id, 1) await eiger_writer.open(name="test_eiger", exposures_per_event=1) resource_docs, data_docs = await collect_docs( num_triggers=await mock_eiger_driver.num_triggers.get_value() @@ -574,7 +575,7 @@ async def _write_file(value: bool, wait: bool) -> None: RE.subscribe(tiled_writer) callback_on_mock_put(mock_eiger_detector.driver.acquire, _write_file) - set_mock_value(mock_eiger_detector.fileio.sequence_id, 1) + set_mock_value(mock_eiger_detector.fileio.sequence_id, 0) set_mock_value(mock_eiger_detector.driver.num_images, 1) set_mock_value(mock_eiger_detector.fileio.array_counter, 0) set_mock_value(mock_eiger_detector.driver.acquire_period, 0.001) From c53b09bd06eaf03a33557bf222bbc2b7225b52aa Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Thu, 17 Jul 2025 14:27:22 -0400 Subject: [PATCH 30/46] More accurate file writing and resource generation --- src/cditools/eiger_async.py | 76 ++++++++++++++++++++----------------- tests/test_eiger_async.py | 53 ++++++++++++++------------ 2 files changed, 69 insertions(+), 60 deletions(-) diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index 0363ef72..8f4b5d31 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -163,7 +163,6 @@ class EigerDriverIO(ADBaseIO, NDFileIO): num_exposures: A[SignalRW[int], PvSuffix.rbv("NumExposures")] acquire_time: A[SignalRW[float], PvSuffix.rbv("AcquireTime")] acquire_period: A[SignalRW[float], PvSuffix.rbv("AcquirePeriod")] - data_type: A[SignalRW[str], PvSuffix.rbv("DataType")] temperature_actual: A[SignalR[float], PvSuffix("TemperatureActual")] max_size_x: A[SignalR[int], PvSuffix("MaxSizeX_RBV")] max_size_y: A[SignalR[int], PvSuffix("MaxSizeY_RBV")] @@ -327,14 +326,16 @@ def __init__( self._file_info: PathInfo | None = None self._datasets: list[HDFDatasetDescription] = [] - self._current_sequence_id: int | None = None - self._composer: HDFDocumentComposer | None = None + self._master_file_path_cache: list[Path] = [] async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataKey]: """Setup file writing for acquisition.""" # Get file path info from path provider self._file_info = self._path_provider() - self._current_sequence_id = await self.fileio.sequence_id.get_value() + 1 + self._master_file_path_cache.clear() + + # Cache for use later + self._exposures_per_event = exposures_per_event # Set the name pattern with $id replacement similar to original name_pattern = f"{self._file_info.filename}_$id" @@ -360,11 +361,13 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK await self.fileio.fw_hdf5_format.set(EigerHDF5Format.LEGACY) # Force the number of images per file to a large number to simplify the logic - await self.fileio.fw_nimgs_per_file.set(self._min_num_images_per_file) - logger.warning( - "Setting fw_nimgs_per_file to %d to force writing to a single HDF5 file", - self._min_num_images_per_file, - ) + num_images_per_file = await self.fileio.fw_nimgs_per_file.get_value() + if num_images_per_file < self._min_num_images_per_file: + await self.fileio.fw_nimgs_per_file.set(self._min_num_images_per_file) + logger.warning( + "Setting fw_nimgs_per_file to %d to force writing to a single HDF5 file", + self._min_num_images_per_file, + ) detector_shape, np_dtype = await asyncio.gather( self._dataset_describer.shape(), @@ -439,8 +442,6 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK ), ] - # Cache for use later - self._exposures_per_event = exposures_per_event frame_datasets = [ HDFDatasetDescription( @@ -452,14 +453,12 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK ) ] + # Cache descriptions for later use self._datasets = master_datasets + frame_datasets - if self._master_file_path is None: - raise ValueError("Master file path is None") - return { ds.data_key: DataKey( - source=self._master_file_path.as_posix(), + source="ADEiger FileWriter", shape=list(ds.shape), dtype="array" if exposures_per_event > 1 or len(ds.shape) > 1 @@ -471,17 +470,17 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK } @property - def _master_file_path(self) -> Path | None: - if self._current_sequence_id is None or self._file_info is None: + async def _master_file_path(self) -> Path | None: + if self._file_info is None: logger.warning( - "No master file path found for sequence id %s and file info %s", - self._current_sequence_id, + "No master file path found for file info %s", self._file_info, ) return None + sequence_id = await self.fileio.sequence_id.get_value() return ( self._file_info.directory_path - / f"{self._file_info.filename}_{self._current_sequence_id}_master.h5" + / f"{self._file_info.filename}_{sequence_id}_master.h5" ) async def collect_stream_docs( @@ -489,19 +488,27 @@ async def collect_stream_docs( ) -> AsyncIterator[StreamAsset]: """Generate stream documents for the written HDF5 files.""" if indices_written: - if not self._composer: - if self._master_file_path is None: - raise ValueError("Master file path is not set") + master_file_path = await self._master_file_path + if master_file_path is None: + raise ValueError("Master file path is not set") + + # Eiger generates a new master file for each trigger + # so we need to create a new composer with a new + # master file path + composer = HDFDocumentComposer( + master_file_path, + self._datasets, + ) + # TODO: Make public + composer._last_emitted = indices_written - 1 - self._composer = HDFDocumentComposer( - self._master_file_path, - self._datasets, - ) + # For later validation + self._master_file_path_cache.append(master_file_path) - for doc in self._composer.stream_resources(): - yield "stream_resource", doc + for doc in composer.stream_resources(): + yield "stream_resource", doc - for doc in self._composer.stream_data(indices_written): + for doc in composer.stream_data(indices_written): yield "stream_datum", doc async def observe_indices_written( @@ -521,13 +528,12 @@ async def get_indices_written(self) -> int: async def close(self) -> None: """Clean up file writing after acquisition and validate files exist.""" - # Check that the master file was written - if self._master_file_path is not None and not self._master_file_path.exists(): - logger.warning("Master file was not written: %s", self._master_file_path) + # Check that the master files were written + for master_file_path in self._master_file_path_cache: + if not master_file_path.exists(): + logger.warning("Master file was not written: %s", master_file_path) - self._composer = None self._file_info = None - self._current_sequence_id = None class EigerController(ADBaseController[EigerDriverIO]): diff --git a/tests/test_eiger_async.py b/tests/test_eiger_async.py index f389dc92..2563028e 100644 --- a/tests/test_eiger_async.py +++ b/tests/test_eiger_async.py @@ -14,6 +14,7 @@ import h5py import numpy as np import pytest +from event_model import StreamDatum, StreamResource from bluesky.callbacks.tiled_writer import TiledWriter from bluesky.run_engine import RunEngine from ophyd_async.core import ( @@ -44,7 +45,6 @@ def write_eiger_hdf5_file( num_triggers: int, num_images: int, sequence_id: int, name: str = "test_eiger" ): - sequence_id += 1 with h5py.File(f"/tmp/test_data/{name}_{sequence_id}_data_000001.h5", "w") as f: f.create_dataset( "data_000001", @@ -170,8 +170,6 @@ async def test_eiger_writer_initialization( assert eiger_writer._path_provider is mock_path_provider assert eiger_writer._dataset_describer is not None assert eiger_writer._file_info is None - assert eiger_writer._current_sequence_id is None - assert eiger_writer._composer is None @pytest.mark.asyncio @@ -208,7 +206,7 @@ async def test_eiger_writer_open( } assert ( description["test_eiger_image"]["source"] - == "/tmp/test_data/test_eiger_1_master.h5" + == "ADEiger FileWriter" ) # Case 2: 4 images per file, 11 images, 2 triggers @@ -239,7 +237,7 @@ async def test_eiger_writer_open( assert data_key["dtype_numpy"] == np.dtype(data_type.lower()).str assert "external" in data_key assert data_key["external"] == "STREAM:" - assert data_key["source"] == "/tmp/test_data/test_eiger_2_master.h5" + assert data_key["source"] == "ADEiger FileWriter" @pytest.mark.asyncio @@ -378,10 +376,12 @@ async def test_eiger_writer_collect_stream_docs( ) -> None: """Test collecting stream documents.""" - async def collect_docs(num_triggers: int): + async def collect_docs(num_triggers: int) -> tuple[list[StreamResource], list[StreamDatum]]: resource_docs = [] data_docs = [] for i in range(1, num_triggers + 1): + sequence_id = await mock_eiger_driver.sequence_id.get_value() + set_mock_value(mock_eiger_driver.sequence_id, sequence_id + 1) async for doc_type, doc in eiger_writer.collect_stream_docs( name="", indices_written=i ): @@ -393,11 +393,8 @@ async def collect_docs(num_triggers: int): set_mock_value(mock_eiger_driver.sequence_id, 0) set_mock_value(mock_eiger_driver.num_images, 1) - set_mock_value(mock_eiger_driver.num_triggers, 1) await eiger_writer.open(name="test_eiger", exposures_per_event=1) - resource_docs, data_docs = await collect_docs( - num_triggers=await mock_eiger_driver.num_triggers.get_value() - ) + resource_docs, data_docs = await collect_docs(num_triggers=1) assert len(resource_docs) == 10 assert len(data_docs) == 10 assert ( @@ -407,18 +404,27 @@ async def collect_docs(num_triggers: int): await eiger_writer.close() - set_mock_value(mock_eiger_driver.num_triggers, 3) - set_mock_value(mock_eiger_driver.sequence_id, 1) await eiger_writer.open(name="test_eiger", exposures_per_event=1) - resource_docs, data_docs = await collect_docs( - num_triggers=await mock_eiger_driver.num_triggers.get_value() - ) - assert len(resource_docs) == 10 + resource_docs, data_docs = await collect_docs(num_triggers=3) + assert len(resource_docs) == 30 assert len(data_docs) == 30 - assert ( - resource_docs[0]["uri"] - == "file://localhost/tmp/test_data/test_eiger_2_master.h5" - ) + # There are 10 different datasets inside a single master file + # 3 triggers, so 30 total resources/datasets + for i in range(10): + assert ( + resource_docs[i]["uri"] + == "file://localhost/tmp/test_data/test_eiger_2_master.h5" + ) + for i in range(10, 20): + assert ( + resource_docs[10]["uri"] + == "file://localhost/tmp/test_data/test_eiger_3_master.h5" + ) + for i in range(20, 30): + assert ( + resource_docs[20]["uri"] + == "file://localhost/tmp/test_data/test_eiger_4_master.h5" + ) @pytest.mark.asyncio @@ -438,8 +444,6 @@ async def test_eiger_writer_close( # Verify the writing was disabled await eiger_writer.close() - assert eiger_writer._composer is None - assert eiger_writer._current_sequence_id is None assert eiger_writer._file_info is None @@ -556,7 +560,8 @@ async def test_eiger_detector_with_RE( async def _write_file(value: bool, wait: bool) -> None: if value: num_images = await mock_eiger_detector.driver.num_images.get_value() - sequence_id = await mock_eiger_detector.fileio.sequence_id.get_value() + sequence_id = await mock_eiger_detector.fileio.sequence_id.get_value() + 1 + set_mock_value(mock_eiger_detector.fileio.sequence_id, sequence_id) await asyncio.sleep( await mock_eiger_detector.driver.acquire_period.get_value() ) @@ -577,7 +582,6 @@ async def _write_file(value: bool, wait: bool) -> None: set_mock_value(mock_eiger_detector.fileio.sequence_id, 0) set_mock_value(mock_eiger_detector.driver.num_images, 1) - set_mock_value(mock_eiger_detector.fileio.array_counter, 0) set_mock_value(mock_eiger_detector.driver.acquire_period, 0.001) uid = RE(bp.count([mock_eiger_detector])) assert uid is not None @@ -676,7 +680,6 @@ async def _write_file(value: bool, wait: bool) -> None: set_mock_value(mock_eiger_detector.fileio.sequence_id, 2) set_mock_value(mock_eiger_detector.driver.num_images, 1) - set_mock_value(mock_eiger_detector.fileio.array_counter, 0) set_mock_value(mock_eiger_detector.driver.acquire_period, 0.001) uid = RE(bp.count([mock_eiger_detector], num=10)) assert uid is not None From dd4f27b7fd9f3ce9fc83c854b296b753a5db47c1 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Thu, 17 Jul 2025 15:13:35 -0400 Subject: [PATCH 31/46] Remove frame_time --- src/cditools/eiger_async.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index 8f4b5d31..d4a3ce2c 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -9,13 +9,12 @@ from logging import getLogger from pathlib import Path from typing import Annotated as A -from typing import Any, cast +from typing import Any import numpy as np # type: ignore[import-not-found] from bluesky.protocols import StreamAsset from event_model import DataKey # type: ignore[import-untyped] from ophyd_async.core import ( - AsyncStatus, DetectorTrigger, HDFDatasetDescription, HDFDocumentComposer, @@ -60,8 +59,6 @@ class NDFileIO(NDArrayBaseIO): array_size0: A[SignalR[int], PvSuffix("ArraySize0")] array_size1: A[SignalR[int], PvSuffix("ArraySize1")] create_directory: A[SignalRW[int], PvSuffix("CreateDirectory")] - - # ============================================================================== @@ -404,13 +401,6 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK dtype_numpy=np.dtype(np.float32).str, chunk_shape=(1,), ), - HDFDatasetDescription( - data_key=f"{name}_frame_time", - dataset="entry/instrument/detector/frame_time", - shape=(exposures_per_event,), - dtype_numpy=np.dtype(np.float32).str, - chunk_shape=(1,), - ), HDFDatasetDescription( data_key=f"{name}_beam_center_x", dataset="entry/instrument/detector/beam_center_x", From cd105928f99b8ce63899a6f879d02f38b4aada98 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Fri, 18 Jul 2025 14:36:19 -0400 Subject: [PATCH 32/46] Add back frame time; Fix HDF link to data --- src/cditools/eiger_async.py | 9 ++++++++- tests/test_eiger_async.py | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index d4a3ce2c..eacadf4d 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -401,6 +401,13 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK dtype_numpy=np.dtype(np.float32).str, chunk_shape=(1,), ), + HDFDatasetDescription( + data_key=f"{name}_frame_time", + dataset="entry/instrument/detector/frame_time", + shape=(exposures_per_event,), + dtype_numpy=np.dtype(np.float32).str, + chunk_shape=(1,), + ), HDFDatasetDescription( data_key=f"{name}_beam_center_x", dataset="entry/instrument/detector/beam_center_x", @@ -436,7 +443,7 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK frame_datasets = [ HDFDatasetDescription( data_key=f"{name}_image", - dataset=f"/entry/data_{1:06d}", + dataset=f"/entry/data/data_{1:06d}", shape=(exposures_per_event, *detector_shape), dtype_numpy=np_dtype, chunk_shape=(1, *detector_shape), diff --git a/tests/test_eiger_async.py b/tests/test_eiger_async.py index 2563028e..a6c757b0 100644 --- a/tests/test_eiger_async.py +++ b/tests/test_eiger_async.py @@ -47,13 +47,13 @@ def write_eiger_hdf5_file( ): with h5py.File(f"/tmp/test_data/{name}_{sequence_id}_data_000001.h5", "w") as f: f.create_dataset( - "data_000001", + "entry/data/data", data=np.zeros((num_triggers * num_images, 2048, 2048), dtype=np.uint16), ) with h5py.File(f"/tmp/test_data/{name}_{sequence_id}_master.h5", "w") as f: f["entry/data/data_000001"] = h5py.ExternalLink( - f"/tmp/test_data/{name}_{sequence_id}_data_000001.h5", "data_000001" + f"/tmp/test_data/{name}_{sequence_id}_data_000001.h5", "/entry/data/data" ) f.create_dataset( "entry/instrument/detector/y_pixel_size", From a6b8049a635fba5ebacb1e14baaec60b9fda48be Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Fri, 18 Jul 2025 14:52:22 -0400 Subject: [PATCH 33/46] Remove leading slash for hdf image entry --- src/cditools/eiger_async.py | 2 +- tests/test_eiger_async.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index eacadf4d..f52b53f2 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -443,7 +443,7 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK frame_datasets = [ HDFDatasetDescription( data_key=f"{name}_image", - dataset=f"/entry/data/data_{1:06d}", + dataset=f"entry/data/data_{1:06d}", shape=(exposures_per_event, *detector_shape), dtype_numpy=np_dtype, chunk_shape=(1, *detector_shape), diff --git a/tests/test_eiger_async.py b/tests/test_eiger_async.py index a6c757b0..e1e717dd 100644 --- a/tests/test_eiger_async.py +++ b/tests/test_eiger_async.py @@ -53,7 +53,7 @@ def write_eiger_hdf5_file( with h5py.File(f"/tmp/test_data/{name}_{sequence_id}_master.h5", "w") as f: f["entry/data/data_000001"] = h5py.ExternalLink( - f"/tmp/test_data/{name}_{sequence_id}_data_000001.h5", "/entry/data/data" + f"/tmp/test_data/{name}_{sequence_id}_data_000001.h5", "entry/data/data" ) f.create_dataset( "entry/instrument/detector/y_pixel_size", From 394a5674deb2dbbb0b192e8771a6da16ab4f4bc9 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Fri, 18 Jul 2025 15:03:05 -0400 Subject: [PATCH 34/46] Update data types to match what is actually written by ADEiger --- src/cditools/eiger_async.py | 18 ++++++++---------- tests/test_eiger_async.py | 12 ++++++------ 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index f52b53f2..582e58b4 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -366,10 +366,7 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK self._min_num_images_per_file, ) - detector_shape, np_dtype = await asyncio.gather( - self._dataset_describer.shape(), - self._dataset_describer.np_datatype(), - ) + detector_shape = await self._dataset_describer.shape() # Add the master file datasets master_datasets = [ @@ -377,14 +374,14 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK data_key=f"{name}_y_pixel_size", dataset="entry/instrument/detector/y_pixel_size", shape=(exposures_per_event,), - dtype_numpy=np.dtype(np.uint8).str, + dtype_numpy=np.dtype(np.float32).str, chunk_shape=(1,), ), HDFDatasetDescription( data_key=f"{name}_x_pixel_size", dataset="entry/instrument/detector/x_pixel_size", shape=(exposures_per_event,), - dtype_numpy=np.dtype(np.uint8).str, + dtype_numpy=np.dtype(np.float32).str, chunk_shape=(1,), ), HDFDatasetDescription( @@ -412,14 +409,14 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK data_key=f"{name}_beam_center_x", dataset="entry/instrument/detector/beam_center_x", shape=(exposures_per_event,), - dtype_numpy=np.dtype(np.uint8).str, + dtype_numpy=np.dtype(np.float32).str, chunk_shape=(1,), ), HDFDatasetDescription( data_key=f"{name}_beam_center_y", dataset="entry/instrument/detector/beam_center_y", shape=(exposures_per_event,), - dtype_numpy=np.dtype(np.uint8).str, + dtype_numpy=np.dtype(np.float32).str, chunk_shape=(1,), ), HDFDatasetDescription( @@ -434,7 +431,7 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK dataset="entry/instrument/detector/detectorSpecific/pixel_mask", # TODO: Maybe only 1 mask? shape=(exposures_per_event, *detector_shape), - dtype_numpy=np.dtype(np.uint8).str, + dtype_numpy=np.dtype(np.uint32).str, chunk_shape=(1, *detector_shape), ), ] @@ -445,7 +442,8 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK data_key=f"{name}_image", dataset=f"entry/data/data_{1:06d}", shape=(exposures_per_event, *detector_shape), - dtype_numpy=np_dtype, + # Always write as uint32 + dtype_numpy=np.dtype(np.uint32).str, chunk_shape=(1, *detector_shape), ) ] diff --git a/tests/test_eiger_async.py b/tests/test_eiger_async.py index e1e717dd..3127fb54 100644 --- a/tests/test_eiger_async.py +++ b/tests/test_eiger_async.py @@ -48,7 +48,7 @@ def write_eiger_hdf5_file( with h5py.File(f"/tmp/test_data/{name}_{sequence_id}_data_000001.h5", "w") as f: f.create_dataset( "entry/data/data", - data=np.zeros((num_triggers * num_images, 2048, 2048), dtype=np.uint16), + data=np.zeros((num_triggers * num_images, 2048, 2048), dtype=np.uint32), ) with h5py.File(f"/tmp/test_data/{name}_{sequence_id}_master.h5", "w") as f: @@ -57,11 +57,11 @@ def write_eiger_hdf5_file( ) f.create_dataset( "entry/instrument/detector/y_pixel_size", - data=np.ones((num_images,), dtype=np.uint8), + data=np.ones((num_images,), dtype=np.float32), ) f.create_dataset( "entry/instrument/detector/x_pixel_size", - data=np.ones((num_images,), dtype=np.uint8), + data=np.ones((num_images,), dtype=np.float32), ) f.create_dataset( "entry/instrument/detector/distance", @@ -77,11 +77,11 @@ def write_eiger_hdf5_file( ) f.create_dataset( "entry/instrument/detector/beam_center_x", - data=np.ones((num_images,), dtype=np.uint8), + data=np.ones((num_images,), dtype=np.float32), ) f.create_dataset( "entry/instrument/detector/beam_center_y", - data=np.ones((num_images,), dtype=np.uint8), + data=np.ones((num_images,), dtype=np.float32), ) f.create_dataset( "entry/instrument/detector/count_time", @@ -89,7 +89,7 @@ def write_eiger_hdf5_file( ) f.create_dataset( "entry/instrument/detector/pixel_mask", - data=np.zeros((num_images, 2048, 2048), dtype=np.uint8), + data=np.zeros((num_images, 2048, 2048), dtype=np.uint32), ) From 26dce6e72875bcb1b1aad8333310ea7e43e8177b Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Fri, 18 Jul 2025 15:27:49 -0400 Subject: [PATCH 35/46] Fixed shapes of data --- src/cditools/eiger_async.py | 19 ++++++------ tests/test_eiger_async.py | 59 +++++++++++++++---------------------- 2 files changed, 33 insertions(+), 45 deletions(-) diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index 582e58b4..8a5bc639 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -373,64 +373,63 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK HDFDatasetDescription( data_key=f"{name}_y_pixel_size", dataset="entry/instrument/detector/y_pixel_size", - shape=(exposures_per_event,), + shape=(), dtype_numpy=np.dtype(np.float32).str, chunk_shape=(1,), ), HDFDatasetDescription( data_key=f"{name}_x_pixel_size", dataset="entry/instrument/detector/x_pixel_size", - shape=(exposures_per_event,), + shape=(), dtype_numpy=np.dtype(np.float32).str, chunk_shape=(1,), ), HDFDatasetDescription( data_key=f"{name}_detector_distance", dataset="entry/instrument/detector/distance", - shape=(exposures_per_event,), + shape=(), dtype_numpy=np.dtype(np.float32).str, chunk_shape=(1,), ), HDFDatasetDescription( data_key=f"{name}_incident_wavelength", dataset="entry/instrument/detector/incident_wavelength", - shape=(exposures_per_event,), + shape=(), dtype_numpy=np.dtype(np.float32).str, chunk_shape=(1,), ), HDFDatasetDescription( data_key=f"{name}_frame_time", dataset="entry/instrument/detector/frame_time", - shape=(exposures_per_event,), + shape=(), dtype_numpy=np.dtype(np.float32).str, chunk_shape=(1,), ), HDFDatasetDescription( data_key=f"{name}_beam_center_x", dataset="entry/instrument/detector/beam_center_x", - shape=(exposures_per_event,), + shape=(), dtype_numpy=np.dtype(np.float32).str, chunk_shape=(1,), ), HDFDatasetDescription( data_key=f"{name}_beam_center_y", dataset="entry/instrument/detector/beam_center_y", - shape=(exposures_per_event,), + shape=(), dtype_numpy=np.dtype(np.float32).str, chunk_shape=(1,), ), HDFDatasetDescription( data_key=f"{name}_count_time", dataset="entry/instrument/detector/count_time", - shape=(exposures_per_event,), + shape=(), dtype_numpy=np.dtype(np.float32).str, chunk_shape=(1,), ), HDFDatasetDescription( data_key=f"{name}_pixel_mask", dataset="entry/instrument/detector/detectorSpecific/pixel_mask", - # TODO: Maybe only 1 mask? - shape=(exposures_per_event, *detector_shape), + shape=(1, *detector_shape), dtype_numpy=np.dtype(np.uint32).str, chunk_shape=(1, *detector_shape), ), diff --git a/tests/test_eiger_async.py b/tests/test_eiger_async.py index 3127fb54..1abaaf80 100644 --- a/tests/test_eiger_async.py +++ b/tests/test_eiger_async.py @@ -43,12 +43,12 @@ def write_eiger_hdf5_file( - num_triggers: int, num_images: int, sequence_id: int, name: str = "test_eiger" + num_images: int, sequence_id: int, name: str = "test_eiger" ): with h5py.File(f"/tmp/test_data/{name}_{sequence_id}_data_000001.h5", "w") as f: f.create_dataset( "entry/data/data", - data=np.zeros((num_triggers * num_images, 2048, 2048), dtype=np.uint32), + data=np.zeros((num_images, 2048, 2048), dtype=np.uint32), ) with h5py.File(f"/tmp/test_data/{name}_{sequence_id}_master.h5", "w") as f: @@ -57,39 +57,39 @@ def write_eiger_hdf5_file( ) f.create_dataset( "entry/instrument/detector/y_pixel_size", - data=np.ones((num_images,), dtype=np.float32), + data=np.ones((), dtype=np.float32), ) f.create_dataset( "entry/instrument/detector/x_pixel_size", - data=np.ones((num_images,), dtype=np.float32), + data=np.ones((), dtype=np.float32), ) f.create_dataset( "entry/instrument/detector/distance", - data=np.ones((num_images,), dtype=np.float32), + data=np.ones((), dtype=np.float32), ) f.create_dataset( "entry/instrument/detector/incident_wavelength", - data=np.ones((num_images,), dtype=np.float32), + data=np.ones((), dtype=np.float32), ) f.create_dataset( "entry/instrument/detector/frame_time", - data=np.ones((num_images,), dtype=np.float32), + data=np.ones((), dtype=np.float32), ) f.create_dataset( "entry/instrument/detector/beam_center_x", - data=np.ones((num_images,), dtype=np.float32), + data=np.ones((), dtype=np.float32), ) f.create_dataset( "entry/instrument/detector/beam_center_y", - data=np.ones((num_images,), dtype=np.float32), + data=np.ones((), dtype=np.float32), ) f.create_dataset( "entry/instrument/detector/count_time", - data=np.ones((num_images,), dtype=np.float32), + data=np.ones((), dtype=np.float32), ) f.create_dataset( "entry/instrument/detector/pixel_mask", - data=np.zeros((num_images, 2048, 2048), dtype=np.uint32), + data=np.zeros((2048, 2048), dtype=np.uint32), ) @@ -187,7 +187,6 @@ async def test_eiger_writer_open( # Case 1: 1 image, 1 trigger set_mock_value(mock_eiger_driver.sequence_id, 0) set_mock_value(mock_eiger_driver.num_images, 1) - set_mock_value(mock_eiger_driver.num_triggers, 1) description = await eiger_writer.open(name="test_eiger", exposures_per_event=1) assert await mock_eiger_driver.fw_enable.get_value() is True @@ -213,7 +212,6 @@ async def test_eiger_writer_open( # Expect 6 files, the first 5 will have 4 images, the last will have 2 set_mock_value(mock_eiger_driver.sequence_id, 1) set_mock_value(mock_eiger_driver.num_images, 11) - set_mock_value(mock_eiger_driver.num_triggers, 2) description = await eiger_writer.open( name="test_eiger", exposures_per_event=await mock_eiger_driver.num_images.get_value(), @@ -234,7 +232,7 @@ async def test_eiger_writer_open( assert tuple(data_key["shape"]) == (11, array_size_x, array_size_y) assert data_key["dtype"] == "array" assert "dtype_numpy" in data_key - assert data_key["dtype_numpy"] == np.dtype(data_type.lower()).str + assert data_key["dtype_numpy"] == np.dtype(np.uint32).str assert "external" in data_key assert data_key["external"] == "STREAM:" assert data_key["source"] == "ADEiger FileWriter" @@ -249,7 +247,6 @@ async def test_eiger_writer_get_indices_written( set_mock_value(mock_eiger_driver.sequence_id, 1) # Case 1: 1 image, 1 trigger - set_mock_value(mock_eiger_driver.num_triggers, 1) set_mock_value(mock_eiger_driver.num_images, 1) set_mock_value(mock_eiger_driver.array_counter, 0) await eiger_writer.open( @@ -261,7 +258,6 @@ async def test_eiger_writer_get_indices_written( assert await eiger_writer.get_indices_written() == 1 # Case 2: 1 image, 5 triggers - set_mock_value(mock_eiger_driver.num_triggers, 5) set_mock_value(mock_eiger_driver.num_images, 1) set_mock_value(mock_eiger_driver.array_counter, 0) await eiger_writer.open( @@ -277,7 +273,6 @@ async def test_eiger_writer_get_indices_written( assert await eiger_writer.get_indices_written() == 5 # Case 3: 5 images, 2 triggers - set_mock_value(mock_eiger_driver.num_triggers, 2) set_mock_value(mock_eiger_driver.num_images, 5) set_mock_value(mock_eiger_driver.array_counter, 0) await eiger_writer.open( @@ -437,7 +432,6 @@ async def test_eiger_writer_close( # Verify the writing was enabled set_mock_value(mock_eiger_driver.sequence_id, 1) set_mock_value(mock_eiger_driver.num_images, 1) - set_mock_value(mock_eiger_driver.num_triggers, 1) await eiger_writer.open(name="test_eiger", exposures_per_event=1) assert await mock_eiger_driver.fw_enable.get_value() is True assert await mock_eiger_driver.save_files.get_value() is True @@ -463,7 +457,6 @@ async def test_eiger_controller_prepare(eiger_controller: EigerController) -> No await eiger_controller.driver.trigger_mode.get_value() == EigerTriggerMode.INTERNAL_SERIES ) - # assert await eiger_controller.driver.num_triggers.get_value() == 1 assert await eiger_controller.driver.num_images.get_value() == 1 assert await eiger_controller.driver.image_mode.get_value() == ADImageMode.MULTIPLE @@ -481,7 +474,6 @@ async def test_eiger_controller_prepare(eiger_controller: EigerController) -> No await eiger_controller.driver.trigger_mode.get_value() == EigerTriggerMode.EXTERNAL_SERIES ) - # assert await eiger_controller.driver.num_triggers.get_value() == 10 assert await eiger_controller.driver.num_images.get_value() == 5 assert await eiger_controller.driver.image_mode.get_value() == ADImageMode.MULTIPLE @@ -499,7 +491,6 @@ async def test_eiger_controller_prepare(eiger_controller: EigerController) -> No await eiger_controller.driver.trigger_mode.get_value() == EigerTriggerMode.EXTERNAL_SERIES ) - # assert await eiger_controller.driver.num_triggers.get_value() == 0 assert await eiger_controller.driver.num_images.get_value() == 1 assert ( await eiger_controller.driver.image_mode.get_value() == ADImageMode.CONTINUOUS @@ -509,7 +500,6 @@ async def test_eiger_controller_prepare(eiger_controller: EigerController) -> No @pytest.mark.asyncio async def test_eiger_detector(mock_eiger_detector: EigerDetector) -> None: set_mock_value(mock_eiger_detector.driver.num_images, 1) - set_mock_value(mock_eiger_detector.driver.num_triggers, 2) set_mock_value(mock_eiger_detector.driver.acquire_period, 0.001) set_mock_value(mock_eiger_detector.fileio.array_counter, 0) @@ -566,7 +556,6 @@ async def _write_file(value: bool, wait: bool) -> None: await mock_eiger_detector.driver.acquire_period.get_value() ) write_eiger_hdf5_file( - num_triggers=1, num_images=num_images, sequence_id=sequence_id, name="test_eiger", @@ -594,7 +583,7 @@ async def _write_file(value: bool, wait: bool) -> None: ) assert ( tiled_client.values().last()["streams"]["primary"]["test_eiger_image"].dtype - == np.uint16 + == np.uint32 ) assert tiled_client.values().last()["streams"]["primary"][ "test_eiger_x_pixel_size" @@ -603,7 +592,7 @@ async def _write_file(value: bool, wait: bool) -> None: tiled_client.values() .last()["streams"]["primary"]["test_eiger_x_pixel_size"] .dtype - == np.uint8 + == np.float32 ) assert tiled_client.values().last()["streams"]["primary"][ "test_eiger_y_pixel_size" @@ -612,7 +601,7 @@ async def _write_file(value: bool, wait: bool) -> None: tiled_client.values() .last()["streams"]["primary"]["test_eiger_y_pixel_size"] .dtype - == np.uint8 + == np.float32 ) assert tiled_client.values().last()["streams"]["primary"][ "test_eiger_detector_distance" @@ -648,7 +637,7 @@ async def _write_file(value: bool, wait: bool) -> None: tiled_client.values() .last()["streams"]["primary"]["test_eiger_beam_center_x"] .dtype - == np.uint8 + == np.float32 ) assert tiled_client.values().last()["streams"]["primary"][ "test_eiger_beam_center_y" @@ -657,7 +646,7 @@ async def _write_file(value: bool, wait: bool) -> None: tiled_client.values() .last()["streams"]["primary"]["test_eiger_beam_center_y"] .dtype - == np.uint8 + == np.float32 ) assert tiled_client.values().last()["streams"]["primary"][ "test_eiger_count_time" @@ -675,7 +664,7 @@ async def _write_file(value: bool, wait: bool) -> None: tiled_client.values() .last()["streams"]["primary"]["test_eiger_pixel_mask"] .dtype - == np.uint8 + == np.uint32 ) set_mock_value(mock_eiger_detector.fileio.sequence_id, 2) @@ -692,7 +681,7 @@ async def _write_file(value: bool, wait: bool) -> None: ) assert ( tiled_client.values().last()["streams"]["primary"]["test_eiger_image"].dtype - == np.uint16 + == np.uint32 ) assert tiled_client.values().last()["streams"]["primary"][ "test_eiger_x_pixel_size" @@ -701,7 +690,7 @@ async def _write_file(value: bool, wait: bool) -> None: tiled_client.values() .last()["streams"]["primary"]["test_eiger_x_pixel_size"] .dtype - == np.uint8 + == np.float32 ) assert tiled_client.values().last()["streams"]["primary"][ "test_eiger_y_pixel_size" @@ -710,7 +699,7 @@ async def _write_file(value: bool, wait: bool) -> None: tiled_client.values() .last()["streams"]["primary"]["test_eiger_y_pixel_size"] .dtype - == np.uint8 + == np.float32 ) assert tiled_client.values().last()["streams"]["primary"][ "test_eiger_detector_distance" @@ -746,7 +735,7 @@ async def _write_file(value: bool, wait: bool) -> None: tiled_client.values() .last()["streams"]["primary"]["test_eiger_beam_center_x"] .dtype - == np.uint8 + == np.float32 ) assert tiled_client.values().last()["streams"]["primary"][ "test_eiger_beam_center_y" @@ -755,7 +744,7 @@ async def _write_file(value: bool, wait: bool) -> None: tiled_client.values() .last()["streams"]["primary"]["test_eiger_beam_center_y"] .dtype - == np.uint8 + == np.float32 ) assert tiled_client.values().last()["streams"]["primary"][ "test_eiger_count_time" @@ -773,5 +762,5 @@ async def _write_file(value: bool, wait: bool) -> None: tiled_client.values() .last()["streams"]["primary"]["test_eiger_pixel_mask"] .dtype - == np.uint8 + == np.uint32 ) From 68098213a0d693323b75a82560aa367368d01ba9 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Fri, 18 Jul 2025 17:15:36 -0400 Subject: [PATCH 36/46] Remove extra metadata until tiled can support readback with () shape --- src/cditools/eiger_async.py | 192 ++++++++++------ tests/conftest.py | 7 +- tests/test_eiger_async.py | 424 +++++++++++++++++++----------------- 3 files changed, 352 insertions(+), 271 deletions(-) diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index 8a5bc639..fb0a5585 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -9,7 +9,7 @@ from logging import getLogger from pathlib import Path from typing import Annotated as A -from typing import Any +from typing import Any, Literal import numpy as np # type: ignore[import-not-found] from bluesky.protocols import StreamAsset @@ -38,10 +38,53 @@ NDPluginBaseIO, ) from ophyd_async.epics.signal import PvSuffix +from urllib.parse import urlunparse + +from event_model import ( # type: ignore + ComposeStreamResource, + ComposeStreamResourceBundle, +) logger = getLogger(__name__) +# TODO: Port to ophyd-async +# ============================================================================== +class HDFDatasetDescription2(HDFDatasetDescription): + join_method: Literal["stack", "concat"] = "concat" + +class HDFDocumentComposer2(HDFDocumentComposer): + def __init__(self, full_file_name: Path, datasets: list[HDFDatasetDescription2], hostname: str = "localhost") -> None: + self._last_emitted = 0 + self._hostname = hostname + uri = urlunparse( + ( + "file", + self._hostname, + str(full_file_name.absolute()), + "", + "", + None, + ) + ) + bundler_composer = ComposeStreamResource() + self._bundles: list[ComposeStreamResourceBundle] = [ + bundler_composer( + mimetype="application/x-hdf5", + uri=uri, + data_key=ds.data_key, + parameters={ + "dataset": ds.dataset, + "chunk_shape": ds.chunk_shape, + "join_method": ds.join_method, + }, + uid=None, + validate=True, + ) + for ds in datasets + ] +# ============================================================================== + # TODO: Port to ophyd-async (https://github.com/bluesky/ophyd-async/issues/961) # ============================================================================== class NDFileIO(NDArrayBaseIO): @@ -322,7 +365,7 @@ def __init__( ) self._file_info: PathInfo | None = None - self._datasets: list[HDFDatasetDescription] = [] + self._datasets: list[HDFDatasetDescription2] = [] self._master_file_path_cache: list[Path] = [] async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataKey]: @@ -368,76 +411,87 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK detector_shape = await self._dataset_describer.shape() + # TODO: Add these when empty shape datasets are supported by tiled # Add the master file datasets - master_datasets = [ - HDFDatasetDescription( - data_key=f"{name}_y_pixel_size", - dataset="entry/instrument/detector/y_pixel_size", - shape=(), - dtype_numpy=np.dtype(np.float32).str, - chunk_shape=(1,), - ), - HDFDatasetDescription( - data_key=f"{name}_x_pixel_size", - dataset="entry/instrument/detector/x_pixel_size", - shape=(), - dtype_numpy=np.dtype(np.float32).str, - chunk_shape=(1,), - ), - HDFDatasetDescription( - data_key=f"{name}_detector_distance", - dataset="entry/instrument/detector/distance", - shape=(), - dtype_numpy=np.dtype(np.float32).str, - chunk_shape=(1,), - ), - HDFDatasetDescription( - data_key=f"{name}_incident_wavelength", - dataset="entry/instrument/detector/incident_wavelength", - shape=(), - dtype_numpy=np.dtype(np.float32).str, - chunk_shape=(1,), - ), - HDFDatasetDescription( - data_key=f"{name}_frame_time", - dataset="entry/instrument/detector/frame_time", - shape=(), - dtype_numpy=np.dtype(np.float32).str, - chunk_shape=(1,), - ), - HDFDatasetDescription( - data_key=f"{name}_beam_center_x", - dataset="entry/instrument/detector/beam_center_x", - shape=(), - dtype_numpy=np.dtype(np.float32).str, - chunk_shape=(1,), - ), - HDFDatasetDescription( - data_key=f"{name}_beam_center_y", - dataset="entry/instrument/detector/beam_center_y", - shape=(), - dtype_numpy=np.dtype(np.float32).str, - chunk_shape=(1,), - ), - HDFDatasetDescription( - data_key=f"{name}_count_time", - dataset="entry/instrument/detector/count_time", - shape=(), - dtype_numpy=np.dtype(np.float32).str, - chunk_shape=(1,), - ), - HDFDatasetDescription( - data_key=f"{name}_pixel_mask", - dataset="entry/instrument/detector/detectorSpecific/pixel_mask", - shape=(1, *detector_shape), - dtype_numpy=np.dtype(np.uint32).str, - chunk_shape=(1, *detector_shape), - ), - ] + master_datasets = [] + #master_datasets = [ + # HDFDatasetDescription2( + # data_key=f"{name}_y_pixel_size", + # dataset="entry/instrument/detector/y_pixel_size", + # shape=(), + # dtype_numpy=np.dtype(np.float32).str, + # chunk_shape=(), + # join_method="stack", + # ), + # HDFDatasetDescription2( + # data_key=f"{name}_x_pixel_size", + # dataset="entry/instrument/detector/x_pixel_size", + # shape=(), + # dtype_numpy=np.dtype(np.float32).str, + # chunk_shape=(), + # join_method="stack", + # ), + # HDFDatasetDescription2( + # data_key=f"{name}_detector_distance", + # dataset="entry/instrument/detector/detector_distance", + # shape=(), + # dtype_numpy=np.dtype(np.float32).str, + # chunk_shape=(), + # join_method="stack", + # ), + # HDFDatasetDescription2( + # data_key=f"{name}_incident_wavelength", + # dataset="entry/instrument/detector/incident_wavelength", + # shape=(), + # dtype_numpy=np.dtype(np.float32).str, + # chunk_shape=(), + # join_method="stack", + # ), + # HDFDatasetDescription2( + # data_key=f"{name}_frame_time", + # dataset="entry/instrument/detector/frame_time", + # shape=(), + # dtype_numpy=np.dtype(np.float32).str, + # chunk_shape=(), + # join_method="stack", + # ), + # HDFDatasetDescription2( + # data_key=f"{name}_beam_center_x", + # dataset="entry/instrument/detector/beam_center_x", + # shape=(), + # dtype_numpy=np.dtype(np.float32).str, + # chunk_shape=(), + # join_method="stack", + # ), + # HDFDatasetDescription2( + # data_key=f"{name}_beam_center_y", + # dataset="entry/instrument/detector/beam_center_y", + # shape=(), + # dtype_numpy=np.dtype(np.float32).str, + # chunk_shape=(), + # join_method="stack", + # ), + # HDFDatasetDescription2( + # data_key=f"{name}_count_time", + # dataset="entry/instrument/detector/count_time", + # shape=(), + # dtype_numpy=np.dtype(np.float32).str, + # chunk_shape=(), + # join_method="stack", + # ), + # HDFDatasetDescription2( + # data_key=f"{name}_pixel_mask", + # dataset="entry/instrument/detector/detectorSpecific/pixel_mask", + # shape=detector_shape, + # dtype_numpy=np.dtype(np.uint32).str, + # chunk_shape=detector_shape, + # join_method="stack", + # ), + #] frame_datasets = [ - HDFDatasetDescription( + HDFDatasetDescription2( data_key=f"{name}_image", dataset=f"entry/data/data_{1:06d}", shape=(exposures_per_event, *detector_shape), @@ -489,7 +543,7 @@ async def collect_stream_docs( # Eiger generates a new master file for each trigger # so we need to create a new composer with a new # master file path - composer = HDFDocumentComposer( + composer = HDFDocumentComposer2( master_file_path, self._datasets, ) diff --git a/tests/conftest.py b/tests/conftest.py index 57953f31..56c77822 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,8 @@ import contextlib import pprint from collections.abc import Generator +import shutil +from pathlib import Path import pytest from bluesky.run_engine import RunEngine, TransitionError @@ -89,7 +91,10 @@ def clean_event_loop(): @pytest.fixture def tiled_client() -> Generator[Container, None, None]: - server: SimpleTiledServer = SimpleTiledServer() + server_path = Path("/tmp/pytest/tiled-server") + if server_path.exists(): + shutil.rmtree(server_path) + server: SimpleTiledServer = SimpleTiledServer("/tmp/pytest/tiled-server", readable_storage=["/tmp"]) client: Container = from_uri(server.uri) yield client server.close() diff --git a/tests/test_eiger_async.py b/tests/test_eiger_async.py index 1abaaf80..c16cf6b8 100644 --- a/tests/test_eiger_async.py +++ b/tests/test_eiger_async.py @@ -64,7 +64,7 @@ def write_eiger_hdf5_file( data=np.ones((), dtype=np.float32), ) f.create_dataset( - "entry/instrument/detector/distance", + "entry/instrument/detector/detector_distance", data=np.ones((), dtype=np.float32), ) f.create_dataset( @@ -88,7 +88,7 @@ def write_eiger_hdf5_file( data=np.ones((), dtype=np.float32), ) f.create_dataset( - "entry/instrument/detector/pixel_mask", + "entry/instrument/detector/detectorSpecific/pixel_mask", data=np.zeros((2048, 2048), dtype=np.uint32), ) @@ -192,15 +192,16 @@ async def test_eiger_writer_open( assert await mock_eiger_driver.fw_enable.get_value() is True assert await mock_eiger_driver.save_files.get_value() is True assert description.keys() == { - "test_eiger_y_pixel_size", - "test_eiger_x_pixel_size", - "test_eiger_detector_distance", - "test_eiger_incident_wavelength", - "test_eiger_frame_time", - "test_eiger_beam_center_x", - "test_eiger_beam_center_y", - "test_eiger_count_time", - "test_eiger_pixel_mask", + # TODO: Add these when empty shape datasets are supported by tiled + # "test_eiger_y_pixel_size", + # "test_eiger_x_pixel_size", + # "test_eiger_detector_distance", + # "test_eiger_incident_wavelength", + # "test_eiger_frame_time", + # "test_eiger_beam_center_x", + # "test_eiger_beam_center_y", + # "test_eiger_count_time", + # "test_eiger_pixel_mask", "test_eiger_image", } assert ( @@ -217,15 +218,16 @@ async def test_eiger_writer_open( exposures_per_event=await mock_eiger_driver.num_images.get_value(), ) assert description.keys() == { - "test_eiger_y_pixel_size", - "test_eiger_x_pixel_size", - "test_eiger_detector_distance", - "test_eiger_incident_wavelength", - "test_eiger_frame_time", - "test_eiger_beam_center_x", - "test_eiger_beam_center_y", - "test_eiger_count_time", - "test_eiger_pixel_mask", + # TODO: Add these when empty shape datasets are supported by tiled + # "test_eiger_y_pixel_size", + # "test_eiger_x_pixel_size", + # "test_eiger_detector_distance", + # "test_eiger_incident_wavelength", + # "test_eiger_frame_time", + # "test_eiger_beam_center_x", + # "test_eiger_beam_center_y", + # "test_eiger_count_time", + # "test_eiger_pixel_mask", "test_eiger_image", } data_key = description["test_eiger_image"] @@ -390,8 +392,8 @@ async def collect_docs(num_triggers: int) -> tuple[list[StreamResource], list[St set_mock_value(mock_eiger_driver.num_images, 1) await eiger_writer.open(name="test_eiger", exposures_per_event=1) resource_docs, data_docs = await collect_docs(num_triggers=1) - assert len(resource_docs) == 10 - assert len(data_docs) == 10 + assert len(resource_docs) == 1 + assert len(data_docs) == 1 assert ( resource_docs[0]["uri"] == "file://localhost/tmp/test_data/test_eiger_1_master.h5" @@ -401,25 +403,22 @@ async def collect_docs(num_triggers: int) -> tuple[list[StreamResource], list[St await eiger_writer.open(name="test_eiger", exposures_per_event=1) resource_docs, data_docs = await collect_docs(num_triggers=3) - assert len(resource_docs) == 30 - assert len(data_docs) == 30 + assert len(resource_docs) == 3 + assert len(data_docs) == 3 # There are 10 different datasets inside a single master file # 3 triggers, so 30 total resources/datasets - for i in range(10): - assert ( - resource_docs[i]["uri"] - == "file://localhost/tmp/test_data/test_eiger_2_master.h5" - ) - for i in range(10, 20): - assert ( - resource_docs[10]["uri"] - == "file://localhost/tmp/test_data/test_eiger_3_master.h5" - ) - for i in range(20, 30): - assert ( - resource_docs[20]["uri"] - == "file://localhost/tmp/test_data/test_eiger_4_master.h5" - ) + assert ( + resource_docs[0]["uri"] + == "file://localhost/tmp/test_data/test_eiger_2_master.h5" + ) + assert ( + resource_docs[1]["uri"] + == "file://localhost/tmp/test_data/test_eiger_3_master.h5" + ) + assert ( + resource_docs[2]["uri"] + == "file://localhost/tmp/test_data/test_eiger_4_master.h5" + ) @pytest.mark.asyncio @@ -545,6 +544,7 @@ async def _simulate_one_trigger(value: bool, wait: bool) -> None: async def test_eiger_detector_with_RE( RE: RunEngine, tiled_client: Container, mock_eiger_detector: EigerDetector ) -> None: + RE.subscribe(print) set_mock_value(mock_eiger_detector.fileio.array_counter, 0) async def _write_file(value: bool, wait: bool) -> None: @@ -574,6 +574,7 @@ async def _write_file(value: bool, wait: bool) -> None: set_mock_value(mock_eiger_detector.driver.acquire_period, 0.001) uid = RE(bp.count([mock_eiger_detector])) assert uid is not None + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_image"].read() is not None assert tiled_client.values().last()["streams"]["primary"][ "test_eiger_image" ].shape == ( @@ -585,93 +586,104 @@ async def _write_file(value: bool, wait: bool) -> None: tiled_client.values().last()["streams"]["primary"]["test_eiger_image"].dtype == np.uint32 ) - assert tiled_client.values().last()["streams"]["primary"][ - "test_eiger_x_pixel_size" - ].shape == (1,) - assert ( - tiled_client.values() - .last()["streams"]["primary"]["test_eiger_x_pixel_size"] - .dtype - == np.float32 - ) - assert tiled_client.values().last()["streams"]["primary"][ - "test_eiger_y_pixel_size" - ].shape == (1,) - assert ( - tiled_client.values() - .last()["streams"]["primary"]["test_eiger_y_pixel_size"] - .dtype - == np.float32 - ) - assert tiled_client.values().last()["streams"]["primary"][ - "test_eiger_detector_distance" - ].shape == (1,) - assert ( - tiled_client.values() - .last()["streams"]["primary"]["test_eiger_detector_distance"] - .dtype - == np.float32 - ) - assert tiled_client.values().last()["streams"]["primary"][ - "test_eiger_incident_wavelength" - ].shape == (1,) - assert ( - tiled_client.values() - .last()["streams"]["primary"]["test_eiger_incident_wavelength"] - .dtype - == np.float32 - ) - assert tiled_client.values().last()["streams"]["primary"][ - "test_eiger_frame_time" - ].shape == (1,) - assert ( - tiled_client.values() - .last()["streams"]["primary"]["test_eiger_frame_time"] - .dtype - == np.float32 - ) - assert tiled_client.values().last()["streams"]["primary"][ - "test_eiger_beam_center_x" - ].shape == (1,) - assert ( - tiled_client.values() - .last()["streams"]["primary"]["test_eiger_beam_center_x"] - .dtype - == np.float32 - ) - assert tiled_client.values().last()["streams"]["primary"][ - "test_eiger_beam_center_y" - ].shape == (1,) - assert ( - tiled_client.values() - .last()["streams"]["primary"]["test_eiger_beam_center_y"] - .dtype - == np.float32 - ) - assert tiled_client.values().last()["streams"]["primary"][ - "test_eiger_count_time" - ].shape == (1,) - assert ( - tiled_client.values() - .last()["streams"]["primary"]["test_eiger_count_time"] - .dtype - == np.float32 - ) - assert tiled_client.values().last()["streams"]["primary"][ - "test_eiger_pixel_mask" - ].shape == (1, 2048, 2048) - assert ( - tiled_client.values() - .last()["streams"]["primary"]["test_eiger_pixel_mask"] - .dtype - == np.uint32 - ) + # TODO: Add these when empty shape datasets are supported by tiled + # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_x_pixel_size"].read() is not None + # assert tiled_client.values().last()["streams"]["primary"][ + # "test_eiger_x_pixel_size" + # ].shape == () + # assert ( + # tiled_client.values() + # .last()["streams"]["primary"]["test_eiger_x_pixel_size"] + # .dtype + # == np.float32 + # ) + # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_y_pixel_size"].read() is not None + # assert tiled_client.values().last()["streams"]["primary"][ + # "test_eiger_y_pixel_size" + # ].shape == () + # assert ( + # tiled_client.values() + # .last()["streams"]["primary"]["test_eiger_y_pixel_size"] + # .dtype + # == np.float32 + # ) + # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_detector_distance"].read() is not None + # assert tiled_client.values().last()["streams"]["primary"][ + # "test_eiger_detector_distance" + # ].shape == () + # assert ( + # tiled_client.values() + # .last()["streams"]["primary"]["test_eiger_detector_distance"] + # .dtype + # == np.float32 + # ) + # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_incident_wavelength"].read() is not None + # assert tiled_client.values().last()["streams"]["primary"][ + # "test_eiger_incident_wavelength" + # ].shape == () + # assert ( + # tiled_client.values() + # .last()["streams"]["primary"]["test_eiger_incident_wavelength"] + # .dtype + # == np.float32 + # ) + # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_frame_time"].read() is not None + # assert tiled_client.values().last()["streams"]["primary"][ + # "test_eiger_frame_time" + # ].shape == () + # assert ( + # tiled_client.values() + # .last()["streams"]["primary"]["test_eiger_frame_time"] + # .dtype + # == np.float32 + # ) + # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_beam_center_x"].read() is not None + # assert tiled_client.values().last()["streams"]["primary"][ + # "test_eiger_beam_center_x" + # ].shape == () + # assert ( + # tiled_client.values() + # .last()["streams"]["primary"]["test_eiger_beam_center_x"] + # .dtype + # == np.float32 + # ) + # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_beam_center_y"].read() is not None + # assert tiled_client.values().last()["streams"]["primary"][ + # "test_eiger_beam_center_y" + # ].shape == () + # assert ( + # tiled_client.values() + # .last()["streams"]["primary"]["test_eiger_beam_center_y"] + # .dtype + # == np.float32 + # ) + # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_count_time"].read() is not None + # assert tiled_client.values().last()["streams"]["primary"][ + # "test_eiger_count_time" + # ].shape == () + # assert ( + # tiled_client.values() + # .last()["streams"]["primary"]["test_eiger_count_time"] + # .dtype + # == np.float32 + # ) + # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_pixel_mask"].read() is not None + # assert tiled_client.values().last()["streams"]["primary"][ + # "test_eiger_pixel_mask" + # ].shape == (2048, 2048) + # assert ( + # tiled_client.values() + # .last()["streams"]["primary"]["test_eiger_pixel_mask"] + # .dtype + # == np.uint32 + # ) set_mock_value(mock_eiger_detector.fileio.sequence_id, 2) set_mock_value(mock_eiger_detector.driver.num_images, 1) set_mock_value(mock_eiger_detector.driver.acquire_period, 0.001) uid = RE(bp.count([mock_eiger_detector], num=10)) assert uid is not None + assert tiled_client.values().last()["streams"]["primary"]["test_eiger_image"].read() is not None assert tiled_client.values().last()["streams"]["primary"][ "test_eiger_image" ].shape == ( @@ -683,84 +695,94 @@ async def _write_file(value: bool, wait: bool) -> None: tiled_client.values().last()["streams"]["primary"]["test_eiger_image"].dtype == np.uint32 ) - assert tiled_client.values().last()["streams"]["primary"][ - "test_eiger_x_pixel_size" - ].shape == (10,) - assert ( - tiled_client.values() - .last()["streams"]["primary"]["test_eiger_x_pixel_size"] - .dtype - == np.float32 - ) - assert tiled_client.values().last()["streams"]["primary"][ - "test_eiger_y_pixel_size" - ].shape == (10,) - assert ( - tiled_client.values() - .last()["streams"]["primary"]["test_eiger_y_pixel_size"] - .dtype - == np.float32 - ) - assert tiled_client.values().last()["streams"]["primary"][ - "test_eiger_detector_distance" - ].shape == (10,) - assert ( - tiled_client.values() - .last()["streams"]["primary"]["test_eiger_detector_distance"] - .dtype - == np.float32 - ) - assert tiled_client.values().last()["streams"]["primary"][ - "test_eiger_incident_wavelength" - ].shape == (10,) - assert ( - tiled_client.values() - .last()["streams"]["primary"]["test_eiger_incident_wavelength"] - .dtype - == np.float32 - ) - assert tiled_client.values().last()["streams"]["primary"][ - "test_eiger_frame_time" - ].shape == (10,) - assert ( - tiled_client.values() - .last()["streams"]["primary"]["test_eiger_frame_time"] - .dtype - == np.float32 - ) - assert tiled_client.values().last()["streams"]["primary"][ - "test_eiger_beam_center_x" - ].shape == (10,) - assert ( - tiled_client.values() - .last()["streams"]["primary"]["test_eiger_beam_center_x"] - .dtype - == np.float32 - ) - assert tiled_client.values().last()["streams"]["primary"][ - "test_eiger_beam_center_y" - ].shape == (10,) - assert ( - tiled_client.values() - .last()["streams"]["primary"]["test_eiger_beam_center_y"] - .dtype - == np.float32 - ) - assert tiled_client.values().last()["streams"]["primary"][ - "test_eiger_count_time" - ].shape == (10,) - assert ( - tiled_client.values() - .last()["streams"]["primary"]["test_eiger_count_time"] - .dtype - == np.float32 - ) - assert tiled_client.values().last()["streams"]["primary"][ - "test_eiger_pixel_mask" - ].shape == (10, 2048, 2048) - assert ( - tiled_client.values() - .last()["streams"]["primary"]["test_eiger_pixel_mask"] - .dtype - == np.uint32 - ) + # TODO: Add these when empty shape datasets are supported by tiled + # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_x_pixel_size"].read() is not None + # assert tiled_client.values().last()["streams"]["primary"][ + # "test_eiger_x_pixel_size" + # ].shape == (10,) + # assert ( + # tiled_client.values() + # .last()["streams"]["primary"]["test_eiger_x_pixel_size"] + # .dtype + # == np.float32 + # ) + # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_y_pixel_size"].read() is not None + # assert tiled_client.values().last()["streams"]["primary"][ + # "test_eiger_y_pixel_size" + # ].shape == (10,) + # assert ( + # tiled_client.values() + # .last()["streams"]["primary"]["test_eiger_y_pixel_size"] + # .dtype + # == np.float32 + # ) + # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_detector_distance"].read() is not None + # assert tiled_client.values().last()["streams"]["primary"][ + # "test_eiger_detector_distance" + # ].shape == (10,) + # assert ( + # tiled_client.values() + # .last()["streams"]["primary"]["test_eiger_detector_distance"] + # .dtype + # == np.float32 + # ) + # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_incident_wavelength"].read() is not None + # assert tiled_client.values().last()["streams"]["primary"][ + # "test_eiger_incident_wavelength" + # ].shape == (10,) + # assert ( + # tiled_client.values() + # .last()["streams"]["primary"]["test_eiger_incident_wavelength"] + # .dtype + # == np.float32 + # ) + # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_frame_time"].read() is not None + # assert tiled_client.values().last()["streams"]["primary"][ + # "test_eiger_frame_time" + # ].shape == (10,) + # assert ( + # tiled_client.values() + # .last()["streams"]["primary"]["test_eiger_frame_time"] + # .dtype + # == np.float32 + # ) + # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_beam_center_x"].read() is not None + # assert tiled_client.values().last()["streams"]["primary"][ + # "test_eiger_beam_center_x" + # ].shape == (10,) + # assert ( + # tiled_client.values() + # .last()["streams"]["primary"]["test_eiger_beam_center_x"] + # .dtype + # == np.float32 + # ) + # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_beam_center_y"].read() is not None + # assert tiled_client.values().last()["streams"]["primary"][ + # "test_eiger_beam_center_y" + # ].shape == (10,) + # assert ( + # tiled_client.values() + # .last()["streams"]["primary"]["test_eiger_beam_center_y"] + # .dtype + # == np.float32 + # ) + # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_count_time"].read() is not None + # assert tiled_client.values().last()["streams"]["primary"][ + # "test_eiger_count_time" + # ].shape == (10,) + # assert ( + # tiled_client.values() + # .last()["streams"]["primary"]["test_eiger_count_time"] + # .dtype + # == np.float32 + # ) + # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_pixel_mask"].read() is not None + # assert tiled_client.values().last()["streams"]["primary"][ + # "test_eiger_pixel_mask" + # ].shape == (10, 2048, 2048) + # assert ( + # tiled_client.values() + # .last()["streams"]["primary"]["test_eiger_pixel_mask"] + # .dtype + # == np.uint32 + # ) From 32245d7799cc8e96fdd675c71169085d52c94402 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Tue, 22 Jul 2025 15:38:37 -0400 Subject: [PATCH 37/46] Pre-commit errors --- .pre-commit-config.yaml | 17 -------- pixi.lock | 4 +- pyproject.toml | 20 +-------- src/cditools/eiger_async.py | 63 +++++++++++++++++----------- tests/conftest.py | 15 ++++--- tests/test_eiger_async.py | 82 ++++++++++++++++++------------------- 6 files changed, 92 insertions(+), 109 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0d156235..16038706 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,13 +27,6 @@ repos: - id: requirements-txt-fixer - id: trailing-whitespace - - repo: https://github.com/pre-commit/pygrep-hooks - rev: "v1.10.0" - hooks: - - id: rst-backticks - - id: rst-directive-colons - - id: rst-inline-touching-normal - - repo: https://github.com/rbubley/mirrors-prettier rev: "v3.5.3" hooks: @@ -57,16 +50,6 @@ repos: types: [python] files: src|tests - - repo: https://github.com/codespell-project/codespell - rev: "v2.4.1" - hooks: - - id: codespell - - - repo: https://github.com/shellcheck-py/shellcheck-py - rev: "v0.10.0.1" - hooks: - - id: shellcheck - - repo: local hooks: - id: disallow-caps diff --git a/pixi.lock b/pixi.lock index 81d6cde3..04dd2939 100644 --- a/pixi.lock +++ b/pixi.lock @@ -997,8 +997,8 @@ packages: requires_python: '>=3.8' - pypi: . name: cditools - version: 0.1.dev63+gc62d9d2 - sha256: 263040a2b9c717fae683c72a50ab6bc1b125d49e1b6d51cc5587063d9544f105 + version: 0.1.dev76+g6809821.d20250722 + sha256: c080375c6ec2ef7fc203adf19c93127ab7b82aa23bc5db1c6ddc30349b52b882 requires_dist: - ophyd - ophyd-async>=0.10.0a4 diff --git a/pyproject.toml b/pyproject.toml index 54046dd0..02ea1691 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,7 +95,6 @@ testpaths = [ "tests", ] - [tool.coverage] run.source = ["cditools"] report.exclude_also = [ @@ -105,7 +104,7 @@ report.exclude_also = [ [tool.pyright] include = ["src", "tests"] -pythonVersion = "3.9" +pythonVersion = "3.12" typeCheckingMode = "strict" reportMissingImports = true # Required for untyped packages @@ -152,20 +151,6 @@ isort.required-imports = ["from __future__ import annotations"] "tests/**" = ["T20", "ARG001"] "noxfile.py" = ["T20"] -[tool.pylint] -py-version = "3.9" -ignore-paths = [".*/_version.py"] -reports.output-format = "colorized" -similarities.ignore-imports = "yes" -messages_control.disable = [ - "design", - "fixme", - "line-too-long", - "missing-module-docstring", - "missing-function-docstring", - "wrong-import-position", -] - [tool.pixi.project] channels = ["conda-forge"] platforms = ["linux-64"] @@ -178,6 +163,3 @@ default = { solve-group = "default" } dev = { features = ["dev"], solve-group = "default" } docs = { features = ["docs"], solve-group = "default" } test = { features = ["test"], solve-group = "default" } - -[tool.codespell] -skip = '*.lock' diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index fb0a5585..d3b752b4 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -10,16 +10,22 @@ from pathlib import Path from typing import Annotated as A from typing import Any, Literal +from urllib.parse import urlunparse import numpy as np # type: ignore[import-not-found] from bluesky.protocols import StreamAsset -from event_model import DataKey # type: ignore[import-untyped] +from event_model import ( # type: ignore[import-untyped] + ComposeStreamResource, + ComposeStreamResourceBundle, + DataKey, # type: ignore[import-untyped] +) from ophyd_async.core import ( DetectorTrigger, HDFDatasetDescription, HDFDocumentComposer, PathInfo, PathProvider, + SignalDatatypeT, SignalR, SignalRW, StrictEnum, @@ -38,12 +44,6 @@ NDPluginBaseIO, ) from ophyd_async.epics.signal import PvSuffix -from urllib.parse import urlunparse - -from event_model import ( # type: ignore - ComposeStreamResource, - ComposeStreamResourceBundle, -) logger = getLogger(__name__) @@ -53,8 +53,14 @@ class HDFDatasetDescription2(HDFDatasetDescription): join_method: Literal["stack", "concat"] = "concat" + class HDFDocumentComposer2(HDFDocumentComposer): - def __init__(self, full_file_name: Path, datasets: list[HDFDatasetDescription2], hostname: str = "localhost") -> None: + def __init__( + self, + full_file_name: Path, + datasets: list[HDFDatasetDescription2], + hostname: str = "localhost", + ) -> None: self._last_emitted = 0 self._hostname = hostname uri = urlunparse( @@ -83,8 +89,11 @@ def __init__(self, full_file_name: Path, datasets: list[HDFDatasetDescription2], ) for ds in datasets ] + + # ============================================================================== + # TODO: Port to ophyd-async (https://github.com/bluesky/ophyd-async/issues/961) # ============================================================================== class NDFileIO(NDArrayBaseIO): @@ -102,6 +111,8 @@ class NDFileIO(NDArrayBaseIO): array_size0: A[SignalR[int], PvSuffix("ArraySize0")] array_size1: A[SignalR[int], PvSuffix("ArraySize1")] create_directory: A[SignalRW[int], PvSuffix("CreateDirectory")] + + # ============================================================================== @@ -341,7 +352,7 @@ class Eiger2DriverIO(EigerDriverIO): fw_hdf5_format: A[SignalRW[EigerHDF5Format], PvSuffix.rbv("FWHDF5Format")] -class EigerWriter(ADWriter[EigerDriverIO]): +class EigerWriter(ADWriter[EigerDriverIO]): # type: ignore[reportInvalidTypeArguments] """Eiger-specific file writer using the built-in FileWriter interface.""" default_suffix: str = "cam1:" @@ -414,7 +425,7 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK # TODO: Add these when empty shape datasets are supported by tiled # Add the master file datasets master_datasets = [] - #master_datasets = [ + # master_datasets = [ # HDFDatasetDescription2( # data_key=f"{name}_y_pixel_size", # dataset="entry/instrument/detector/y_pixel_size", @@ -487,8 +498,7 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK # chunk_shape=detector_shape, # join_method="stack", # ), - #] - + # ] frame_datasets = [ HDFDatasetDescription2( @@ -538,7 +548,8 @@ async def collect_stream_docs( if indices_written: master_file_path = await self._master_file_path if master_file_path is None: - raise ValueError("Master file path is not set") + msg = f"Master file path is not set for {name}: {self._file_info}" + raise ValueError(msg) # Eiger generates a new master file for each trigger # so we need to create a new composer with a new @@ -548,7 +559,7 @@ async def collect_stream_docs( self._datasets, ) # TODO: Make public - composer._last_emitted = indices_written - 1 + composer._last_emitted = indices_written - 1 # type: ignore[reportPrivateUsage] # For later validation self._master_file_path_cache.append(master_file_path) @@ -562,16 +573,11 @@ async def collect_stream_docs( async def observe_indices_written( self, timeout: float ) -> AsyncGenerator[int, None]: - async for num_captured in observe_value( - self.fileio.array_counter, timeout - ): + async for num_captured in observe_value(self.fileio.array_counter, timeout): yield num_captured // self._exposures_per_event async def get_indices_written(self) -> int: - return ( - await self.fileio.array_counter.get_value() - // self._exposures_per_event - ) + return await self.fileio.array_counter.get_value() // self._exposures_per_event async def close(self) -> None: """Clean up file writing after acquisition and validate files exist.""" @@ -592,9 +598,16 @@ def __init__( ) -> None: super().__init__(driver, *args, **kwargs) - def get_deadtime(self, _exposure: float | None) -> float: + def get_deadtime(self, exposure: float | None) -> float: """Get detector deadtime for the given exposure.""" - return 0.001 + default_deadtime = 0.000001 + if exposure is not None: + logger.warning( + "Ignoring exposure to calculate deadtime: %s, defaulting to %s", + exposure, + default_deadtime, + ) + return default_deadtime async def prepare(self, trigger_info: TriggerInfo) -> None: """Prepare the detector for acquisition.""" @@ -637,10 +650,10 @@ def __init__( prefix: str, path_provider: PathProvider, driver_suffix: str = "cam1:", - writer_cls: type[ADWriter] = EigerWriter, + writer_cls: type[ADWriter] = EigerWriter, # type: ignore[reportUnknownParameterType] fileio_suffix: str | None = None, name: str = "", - config_sigs: Sequence[SignalR] = (), + config_sigs: Sequence[SignalR[SignalDatatypeT]] = (), plugins: dict[str, NDPluginBaseIO] | None = None, ): driver = EigerDriverIO(prefix + driver_suffix) diff --git a/tests/conftest.py b/tests/conftest.py index 56c77822..f3d87c17 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,12 +3,14 @@ import asyncio import contextlib import pprint -from collections.abc import Generator import shutil +from collections.abc import Generator from pathlib import Path +from typing import Any import pytest -from bluesky.run_engine import RunEngine, TransitionError +from bluesky._vendor.super_state_machine.errors import TransitionError +from bluesky.run_engine import RunEngine from tiled.client import from_uri from tiled.client.container import Container from tiled.server.simple import SimpleTiledServer @@ -21,7 +23,7 @@ # ================================================================================== def _error_and_kill_pending_tasks( loop: asyncio.AbstractEventLoop, test_name: str, test_passed: bool -) -> set[asyncio.Task]: +) -> set[asyncio.Task[Any]]: """Cancels pending tasks in the event loop for a test. Raises an exception if the test hasn't already. @@ -73,7 +75,7 @@ def clean_event_loop(): RE.halt() loop.call_soon_threadsafe(loop.stop) - RE._th.join() + RE._th.join() # type: ignore[reportPrivateUsage] try: _error_and_kill_pending_tasks( @@ -94,7 +96,10 @@ def tiled_client() -> Generator[Container, None, None]: server_path = Path("/tmp/pytest/tiled-server") if server_path.exists(): shutil.rmtree(server_path) - server: SimpleTiledServer = SimpleTiledServer("/tmp/pytest/tiled-server", readable_storage=["/tmp"]) + server: SimpleTiledServer = SimpleTiledServer( + server_path.as_posix(), + readable_storage=["/tmp/pytest"], # type: ignore[reportArgumentType] + ) client: Container = from_uri(server.uri) yield client server.close() diff --git a/tests/test_eiger_async.py b/tests/test_eiger_async.py index c16cf6b8..8cd2d965 100644 --- a/tests/test_eiger_async.py +++ b/tests/test_eiger_async.py @@ -4,7 +4,6 @@ from __future__ import annotations -import os import asyncio import shutil from collections.abc import Generator @@ -14,9 +13,9 @@ import h5py import numpy as np import pytest -from event_model import StreamDatum, StreamResource from bluesky.callbacks.tiled_writer import TiledWriter from bluesky.run_engine import RunEngine +from event_model import StreamDatum, StreamResource from ophyd_async.core import ( DetectorTrigger, PathProvider, @@ -25,7 +24,7 @@ TriggerInfo, init_devices, ) -from ophyd_async.epics.adcore import ADBaseDatasetDescriber, ADImageMode +from ophyd_async.epics.adcore import ADBaseDatasetDescriber, ADBaseDataType, ADImageMode from ophyd_async.testing import ( callback_on_mock_put, set_mock_value, @@ -41,19 +40,19 @@ EigerWriter, ) +EIGER_DATA_PATH = Path("/tmp/pytest/eiger_data/") -def write_eiger_hdf5_file( - num_images: int, sequence_id: int, name: str = "test_eiger" -): - with h5py.File(f"/tmp/test_data/{name}_{sequence_id}_data_000001.h5", "w") as f: + +def write_eiger_hdf5_file(num_images: int, sequence_id: int, name: str = "test_eiger"): + with h5py.File(f"{EIGER_DATA_PATH}/{name}_{sequence_id}_data_000001.h5", "w") as f: f.create_dataset( "entry/data/data", data=np.zeros((num_images, 2048, 2048), dtype=np.uint32), ) - with h5py.File(f"/tmp/test_data/{name}_{sequence_id}_master.h5", "w") as f: + with h5py.File(f"{EIGER_DATA_PATH}/{name}_{sequence_id}_master.h5", "w") as f: f["entry/data/data_000001"] = h5py.ExternalLink( - f"/tmp/test_data/{name}_{sequence_id}_data_000001.h5", "entry/data/data" + f"{EIGER_DATA_PATH}/{name}_{sequence_id}_data_000001.h5", "entry/data/data" ) f.create_dataset( "entry/instrument/detector/y_pixel_size", @@ -95,11 +94,10 @@ def write_eiger_hdf5_file( @pytest.fixture def mock_eiger_detector(RE: RunEngine) -> Generator[EigerDetector, None, None]: - data_path = Path("/tmp/test_data/") - if not data_path.exists(): - data_path.mkdir(parents=True) + if not EIGER_DATA_PATH.exists(): + EIGER_DATA_PATH.mkdir(parents=True) path_provider = StaticPathProvider( - StaticFilenameProvider("test_eiger"), directory_path="/tmp/test_data" + StaticFilenameProvider("test_eiger"), directory_path=EIGER_DATA_PATH ) with init_devices(mock=True): detector = EigerDetector("MOCK:EIGER:", path_provider, name="test_eiger") @@ -110,9 +108,8 @@ def mock_eiger_detector(RE: RunEngine) -> Generator[EigerDetector, None, None]: yield detector - data_path = Path("/tmp/test_data/") - if data_path.exists(): - shutil.rmtree(data_path) + if EIGER_DATA_PATH.exists(): + shutil.rmtree(EIGER_DATA_PATH) @pytest.fixture @@ -125,7 +122,7 @@ def mock_eiger_driver(RE: RunEngine) -> EigerDriverIO: set_mock_value(driver.file_path_exists, True) set_mock_value(driver.array_size_x, 2048) set_mock_value(driver.array_size_y, 2048) - set_mock_value(driver.data_type, "UInt16") + set_mock_value(driver.data_type, ADBaseDataType.UINT16) return driver @@ -134,7 +131,7 @@ def mock_eiger_driver(RE: RunEngine) -> EigerDriverIO: def mock_path_provider() -> PathProvider: """Create a mock path provider for testing.""" return StaticPathProvider( - StaticFilenameProvider("test_eiger"), directory_path="/tmp/test_data" + StaticFilenameProvider("test_eiger"), directory_path=EIGER_DATA_PATH ) @@ -144,14 +141,13 @@ def eiger_writer( mock_path_provider: PathProvider, ) -> Generator[EigerWriter, None, None]: """Create an EigerWriter instance for testing.""" - data_path = Path("/tmp/test_data/") - if not data_path.exists(): - data_path.mkdir(parents=True) - assert os.path.exists("/tmp/test_data") + if not EIGER_DATA_PATH.exists(): + EIGER_DATA_PATH.mkdir(parents=True) + assert EIGER_DATA_PATH.exists() dataset_describer = ADBaseDatasetDescriber(mock_eiger_driver) yield EigerWriter(mock_eiger_driver, mock_path_provider, dataset_describer) - if data_path.exists(): - shutil.rmtree(data_path) + if EIGER_DATA_PATH.exists(): + shutil.rmtree(EIGER_DATA_PATH) @pytest.fixture @@ -167,9 +163,9 @@ async def test_eiger_writer_initialization( ): """Test that EigerWriter initializes correctly.""" assert eiger_writer.fileio is mock_eiger_driver - assert eiger_writer._path_provider is mock_path_provider - assert eiger_writer._dataset_describer is not None - assert eiger_writer._file_info is None + assert eiger_writer._path_provider is mock_path_provider # type: ignore[reportPrivateUsage] + assert eiger_writer._dataset_describer is not None # type: ignore[reportPrivateUsage] + assert eiger_writer._file_info is None # type: ignore[reportPrivateUsage] @pytest.mark.asyncio @@ -178,10 +174,9 @@ async def test_eiger_writer_open( mock_eiger_driver: EigerDriverIO, ) -> None: """Test the open method configures the detector correctly.""" - array_size_x, array_size_y, data_type = await asyncio.gather( + array_size_x, array_size_y = await asyncio.gather( mock_eiger_driver.array_size_x.get_value(), mock_eiger_driver.array_size_y.get_value(), - mock_eiger_driver.data_type.get_value(), ) # Case 1: 1 image, 1 trigger @@ -204,10 +199,7 @@ async def test_eiger_writer_open( # "test_eiger_pixel_mask", "test_eiger_image", } - assert ( - description["test_eiger_image"]["source"] - == "ADEiger FileWriter" - ) + assert description["test_eiger_image"]["source"] == "ADEiger FileWriter" # Case 2: 4 images per file, 11 images, 2 triggers # Expect 6 files, the first 5 will have 4 images, the last will have 2 @@ -373,7 +365,9 @@ async def test_eiger_writer_collect_stream_docs( ) -> None: """Test collecting stream documents.""" - async def collect_docs(num_triggers: int) -> tuple[list[StreamResource], list[StreamDatum]]: + async def collect_docs( + num_triggers: int, + ) -> tuple[list[StreamResource], list[StreamDatum]]: resource_docs = [] data_docs = [] for i in range(1, num_triggers + 1): @@ -396,7 +390,7 @@ async def collect_docs(num_triggers: int) -> tuple[list[StreamResource], list[St assert len(data_docs) == 1 assert ( resource_docs[0]["uri"] - == "file://localhost/tmp/test_data/test_eiger_1_master.h5" + == f"file://localhost/{EIGER_DATA_PATH}/test_eiger_1_master.h5" ) await eiger_writer.close() @@ -409,15 +403,15 @@ async def collect_docs(num_triggers: int) -> tuple[list[StreamResource], list[St # 3 triggers, so 30 total resources/datasets assert ( resource_docs[0]["uri"] - == "file://localhost/tmp/test_data/test_eiger_2_master.h5" + == f"file://localhost/{EIGER_DATA_PATH}/test_eiger_2_master.h5" ) assert ( resource_docs[1]["uri"] - == "file://localhost/tmp/test_data/test_eiger_3_master.h5" + == f"file://localhost/{EIGER_DATA_PATH}/test_eiger_3_master.h5" ) assert ( resource_docs[2]["uri"] - == "file://localhost/tmp/test_data/test_eiger_4_master.h5" + == f"file://localhost/{EIGER_DATA_PATH}/test_eiger_4_master.h5" ) @@ -437,7 +431,7 @@ async def test_eiger_writer_close( # Verify the writing was disabled await eiger_writer.close() - assert eiger_writer._file_info is None + assert eiger_writer._file_info is None # type: ignore[reportPrivateUsage] @pytest.mark.asyncio @@ -574,7 +568,10 @@ async def _write_file(value: bool, wait: bool) -> None: set_mock_value(mock_eiger_detector.driver.acquire_period, 0.001) uid = RE(bp.count([mock_eiger_detector])) assert uid is not None - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_image"].read() is not None + assert ( + tiled_client.values().last()["streams"]["primary"]["test_eiger_image"].read() + is not None + ) assert tiled_client.values().last()["streams"]["primary"][ "test_eiger_image" ].shape == ( @@ -683,7 +680,10 @@ async def _write_file(value: bool, wait: bool) -> None: set_mock_value(mock_eiger_detector.driver.acquire_period, 0.001) uid = RE(bp.count([mock_eiger_detector], num=10)) assert uid is not None - assert tiled_client.values().last()["streams"]["primary"]["test_eiger_image"].read() is not None + assert ( + tiled_client.values().last()["streams"]["primary"]["test_eiger_image"].read() + is not None + ) assert tiled_client.values().last()["streams"]["primary"][ "test_eiger_image" ].shape == ( From eefc1ac50aa5aa49de770ffb457263a629168e50 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Tue, 22 Jul 2025 15:40:15 -0400 Subject: [PATCH 38/46] Fixed small test issue --- tests/test_eiger_async.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_eiger_async.py b/tests/test_eiger_async.py index 8cd2d965..cec1778e 100644 --- a/tests/test_eiger_async.py +++ b/tests/test_eiger_async.py @@ -390,7 +390,7 @@ async def collect_docs( assert len(data_docs) == 1 assert ( resource_docs[0]["uri"] - == f"file://localhost/{EIGER_DATA_PATH}/test_eiger_1_master.h5" + == f"file://localhost{EIGER_DATA_PATH}/test_eiger_1_master.h5" ) await eiger_writer.close() @@ -403,15 +403,15 @@ async def collect_docs( # 3 triggers, so 30 total resources/datasets assert ( resource_docs[0]["uri"] - == f"file://localhost/{EIGER_DATA_PATH}/test_eiger_2_master.h5" + == f"file://localhost{EIGER_DATA_PATH}/test_eiger_2_master.h5" ) assert ( resource_docs[1]["uri"] - == f"file://localhost/{EIGER_DATA_PATH}/test_eiger_3_master.h5" + == f"file://localhost{EIGER_DATA_PATH}/test_eiger_3_master.h5" ) assert ( resource_docs[2]["uri"] - == f"file://localhost/{EIGER_DATA_PATH}/test_eiger_4_master.h5" + == f"file://localhost{EIGER_DATA_PATH}/test_eiger_4_master.h5" ) From 67416927799cd73bc885b562c01457f26a387395 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Tue, 22 Jul 2025 15:57:55 -0400 Subject: [PATCH 39/46] Use pixi for pre-commit --- .github/workflows/ci.yml | 13 ++++++++----- pixi.lock | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7ae8150..43ac1089 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,12 +27,15 @@ jobs: with: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-python@v5 - with: - python-version: "3.x" - - name: Install package - run: python -m pip install .[dev] + - name: Setup Pixi + uses: prefix-dev/setup-pixi@v0.8.8 + with: + environments: dev + frozen: true + locked: false + cache: false + activate-environment: true - name: Run pre-commit run: pre-commit run --all-files diff --git a/pixi.lock b/pixi.lock index 04dd2939..7f248440 100644 --- a/pixi.lock +++ b/pixi.lock @@ -997,7 +997,7 @@ packages: requires_python: '>=3.8' - pypi: . name: cditools - version: 0.1.dev76+g6809821.d20250722 + version: 0.1.dev78+geefc1ac.d20250722 sha256: c080375c6ec2ef7fc203adf19c93127ab7b82aa23bc5db1c6ddc30349b52b882 requires_dist: - ophyd From efdaff6659bd14eaba3c491355673bf55befd836 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Tue, 22 Jul 2025 16:47:42 -0400 Subject: [PATCH 40/46] Change test -> dev for unit tests --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 43ac1089..53c94eec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,7 +62,7 @@ jobs: allow-prereleases: true - name: Install package - run: python -m pip install .[test] + run: python -m pip install .[dev] - name: Test package run: >- From d0496966082353be49c744300651e601af81c8ee Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Tue, 2 Sep 2025 09:38:16 -0400 Subject: [PATCH 41/46] Updated versions --- pixi.lock | 1506 ++++++++++++++++++++++++++++------------------------- 1 file changed, 796 insertions(+), 710 deletions(-) diff --git a/pixi.lock b/pixi.lock index 7f248440..7da661e5 100644 --- a/pixi.lock +++ b/pixi.lock @@ -10,68 +10,68 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.1.31-hbd8a1cb_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.2.0-h767d61c_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.2.0-h69a702a_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.2.0-h767d61c_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-h4bc722e_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.49.1-hee588c1_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.0-h7b32b05_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.3-hf636f53_101_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-7_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.5-hec9711d_102_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fc/30/f7da949893bf0d8525acbaee256cf55430332852225f17e18a7f8783dd6f/bluesky-1.14.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/41/497f55b454aa64e2cb3f27990e5cb76c64457d54365b07d6c125bdac7b38/bluesky-1.14.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-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/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b1/f2/47e3f0e06c3bc3834db2a4ed01641e1ce493108461b9540dd5d2fb60d334/event_model-1.22.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/32/e31e3363bf48ad2ba80b644b01ad9676ce154f1b755950de81eb4ed5b6bd/event_model-1.23.1-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/fe/5e/3be305568fe5f34448807976dc82fc151d76c3e0e03958f34770286278c1/flexparser-0.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/41/bc2df86b72965775f6d621e0ee269a5f3ac23e8f870abf519de9c7d93b4d/h5py-3.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e7/ec/86f59025306dcc6deee5fda54d980d077075b8d9889aac80f158bd585f1b/h5py-3.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_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/79/9d/0fb148dc4d6fa4a7dd1d8378168d9b4cd8d4560a6fbf6f0121c5fc34eb68/importlib_metadata-8.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/97/12/a1f2f4fdc6b7159c0d12249456f9fe454665b6126e98dbee9f2bd3cf735c/lz4-4.4.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/2d/7b/2c1d74ca6c94f70a1add74a8393a0138172207dc5de6fc6269483519d048/msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/20/22/2ebae7ae43cd8f2debc35c631172ddf14e2a87ffcc04cf43ff9df9fff0d3/msgpack-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.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/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/fc/ebfd32c3e124e6a1043e19c0ab0769818aa69050ce5589b63d05ff185526/numpy-2.2.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/12/f2/89ea3361a305466bc6460a532188830351220b5f0851a5fa133155c16eca/opentelemetry_api-1.32.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f2/5e/f5ded172af40b7e7ae60697a2b2096cec53e45344edce41e91a74425473c/ophyd-1.10.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/85/38/6b455b1d6f0b9aa3db4e8888b797f77d856a8350d836c66a43d1078573b2/ophyd_async-0.10.0a4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/bb/ee/6b08dde0a022c463b88f55ae81149584b125a42183407dc1045c486cc870/opentelemetry_api-1.36.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fa/b3/1e63820f1a4df7854e33f0c3c5fa6be758c7fb7ea007ae9c8b9b35f48690/ophyd-1.10.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c8/cb/75a17d45ae070fa4af0b6bf79fb820b9d9137bf09f485a6728f71e4a062e/ophyd_async-0.12.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b7/16/bd2f5904557265882108dc2e04f18abc05ab0c2b7082ae9430091daf1d5c/Pint-0.24.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/76/cc/c528311d798e22ec884b816e8aa2989e0f1f28cdc8e5969e2be5f10bce85/pint-0.25-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.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/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/05/7c/8abb70f9017a231c6c961a8941403ed6557664c0913e1bf413cbdc039e75/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/04/9e/91fe10ea09e2383f0d0531cf3da765fa01b3b9918e85df92081813259cf4/ruamel.yaml-0.18.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/03/36/0a14aebbaa26fe7fab4780c76f2239e76cc95a0090bdb25e31d95c492fcd/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/d1/e5/f2a0621f1781b76a38194acae72f01e37b1941470407345b6e8653ad7640/ruamel.yaml-0.18.15-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1f/8f/ecfbe2123ade605c49ef769788f79c38ddb1c8fa81e01f4dbf5cf1a44b16/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/ba/d03f7ee711391af1d5f4dd7c44f8abdd06bce247028af2441ba8f6ff329b/stamina-25.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/03/98/eb27cc78ad3af8e302c9d8ff4977f5026676e130d28dd7578132a457170c/toolz-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl - - pypi: . + - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl + - pypi: ./ dev: channels: - url: https://conda.anaconda.org/conda-forge/ @@ -82,90 +82,89 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.1.31-hbd8a1cb_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.2.0-h767d61c_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.2.0-h69a702a_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.2.0-h767d61c_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-h4bc722e_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.49.1-hee588c1_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.0-h7b32b05_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.3-hf636f53_101_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-7_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.5-hec9711d_102_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - - pypi: https://files.pythonhosted.org/packages/d3/49/b5e260deff3d218a17fe23a1313bb3c033d846bf74505c297f74d2c8abfe/adbc_driver_manager-1.6.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/c1/45/2d3514fd2bf7ee7e1e28c45fdb70f2a27db2f8cc8af7e7c28f846dd7b99d/adbc_driver_postgresql-1.6.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/92/ec/ce41f3c84b5b4f762e4de9072574714c623e959924bf1bbaf1548612cb6e/adbc_driver_sqlite-1.6.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a2/22/44738b41bb5ca30f94b5f4c00c71c20be86d7eb4ddc389d4cf3c7b8b69ef/adbc_driver_manager-1.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/58/15/86561628738161017273d9a689e9405e4ea9a9d41a70fd2460dbc5d646ae/adbc_driver_postgresql-1.7.0-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/93/1f/618d88542ca66baf6bc25a3e5ecbd698eff31b12b2ab2a590bae8d9d8c83/adbc_driver_sqlite-1.7.0-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/5b/08/185c3b29b0698328b202e6c965c23187e2e29ead78cb468aab0a09ee97fc/aioca-1.8.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dd/e2/88e425adac5ad887a087c38d04fe2030010572a3e0e627f8a6e8c33eeda8/alembic-1.16.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/39/4a/4c61d4c84cfd9befb6fa08a702535b27b21fff08c946bc2f6139decbf7f7/alembic-1.16.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2d/6a/885bc91484e1aa8f618f6f0228d76d0e67000b0fdd6090673b777e311913/asciitree-0.3.3.tar.gz + - pypi: https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/ab/6936e2663c47a926e0659437b9333ad87d1ff49b1375d239026e0a268eba/asgi_correlation_id-4.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/96/19/139227a6e67f407b9c386cb594d9628c6c78c9024f26df87c912fabd4368/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d2/af/92435ec3cfe35bd738a3ba2332c8cbdb50f0cc66d1f9943a1a457990fcd6/awkward-2.8.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2b/cf/9424352bd9568bd56eb88e5fc8400a6c5bd3c827fdeeae202fcaaf3c1910/awkward_cpp-46-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/c9/38/8815054e054e45fda3c9d1e25ac77e6387d10d8e33c64bcf79f432b3c4fa/blosc2-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/fc/30/f7da949893bf0d8525acbaee256cf55430332852225f17e18a7f8783dd6f/bluesky-1.14.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/00/f0/2ef431fe4141f5e334759d73e81120492b23b2824336883a91ac04ba710b/cachetools-6.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/57/19/c547e21267fb9bbeb8acde6996fcc21ff4a76eed182e9a9a441480992f57/awkward-2.8.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/a9/87c1a3c29f2378074755f254ed0fe478f0c5ecf2eea94f450d6b0bcee33f/awkward_cpp-48-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/25/70/020cc09340973f361fcfbcfa2933fdb6301170edbe17215e24a5351d4c72/blosc2-3.7.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c1/41/497f55b454aa64e2cb3f27990e5cb76c64457d54365b07d6c125bdac7b38/bluesky-1.14.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6c/56/3124f61d37a7a4e7cc96afc5492c78ba0cb551151e530b54669ddd1436ef/cachetools-6.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/54/346f681c24a9c3a08e2e74dcee2555ccd1081705b46f791f7b228e177d06/canonicaljson-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e9/ad/0ce6cef4823d6c44b87e9b26b2cb284f6e25143ea0ab8215ac836bda825e/caproto-1.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/e8/64c37fadfc2816a7701fa8a6ed8d87327c7d54eacfbfb6edab14a2f2be75/cloudpickle-3.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-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/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/ec/ef/3333295ed58d900a13c92806b67e62f27876845a9a908c939f040887cca9/cryptography-45.0.5-cp311-abi3-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/73/dd/508420fb47d09d904d962f123221bc249f64b5e56aa93d5f5f7603be475f/coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/01/cf/32f019be5de9f6e180926a50ee5f08648e686c7d9a59f2c5d0806a77b1c7/crc32c-2.7.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/96/b8/bca71059e79a0bb2f8e4ec61d9c205fbe97876318566cde3b5092529faa9/cryptography-45.0.7-cp311-abi3-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/38/30/53b0844a7a4c6b041b111b24ca15cc9b8661a86fe1f6aaeb2d0d7f0fb1f2/dask-2025.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/f9/3e04725358c17329652da8c1b2dbd88de723f3dc78bf52ca6d28d52c9279/dask-2025.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/11/79/479e2194c9096b92aecdf33634ae948d2be306c6011673e98ee1917f32c2/dpkt-1.9.8-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/90/a6/c8577b741974f106e24f8eb3efedc399be1a23cbbdcf49dd4bea5bb8aa4e/duckdb-1.3.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/68/fc/259a54fc22111a847981927aa58528d766e8b228c6d41deb0ad8a1959f9f/duckdb-1.3.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/cb/a3/460c57f094a4a165c84a1341c373b0a4f5ec6ac244b998d5021aade89b77/ecdsa-0.19.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/35/a8/365059bbcd4572cbc41de17fd5b682be5868b218c3c5479071865cab9078/entrypoints-0.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4a/18/3853de30cc92c164eccff6e62599ef88d0aa3b64740973b63e13f88145de/epicscorelibs-7.0.7.99.1.1.tar.gz + - pypi: https://files.pythonhosted.org/packages/a2/af/6bb0e1de4fb7218fa4ba95fe91ba1fd4a5661dd28a461eff6a0f609c996c/epicscorelibs-7.0.7.99.1.2-cp313-cp313-manylinux2014_x86_64.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/b1/f2/47e3f0e06c3bc3834db2a4ed01641e1ce493108461b9540dd5d2fb60d334/event_model-1.22.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/53/50/b1222562c6d270fea83e9c9075b8e8600b8479150a18e4516a6138b980d1/fastapi-0.115.14-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/61/bf/fd60001b3abc5222d8eaa4a204cd8c0ae78e75adc688f33ce4bf25b7fafa/fasteners-0.19-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/32/e31e3363bf48ad2ba80b644b01ad9676ce154f1b755950de81eb4ed5b6bd/event_model-1.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/47/d63c60f59a59467fda0f93f46335c9d18526d7071f025cb5b89d5353ea42/fastapi-0.116.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-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/fe/5e/3be305568fe5f34448807976dc82fc151d76c3e0e03958f34770286278c1/flexparser-0.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bb/61/78c7b3851add1481b048b5fdc29067397a1784e2910592bc81bb3f608635/fsspec-2025.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/26/f2/ad51331a157c7015c675702e2d5230c243695c788f8f75feba1af32b3617/greenlet-3.2.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2f/e0/014d5d9d7a4564cf1c40b5039bc882db69fd881111e03ab3657ac0b218e2/fsspec-2025.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4e/27/ca28b77f4cf613282e4ea9bac360cdfa8db8b4d2154e2f0bd82fd26baeb3/h5netcdf-1.6.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/41/bc2df86b72965775f6d621e0ee269a5f3ac23e8f870abf519de9c7d93b4d/h5py-3.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/5b/f5/ac71e692aad076d50a0f5f073204346d5f5577daffd21bb4b72c485f8959/h5netcdf-1.6.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/ec/86f59025306dcc6deee5fda54d980d077075b8d9889aac80f158bd585f1b/h5py-3.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/ba/db/8d85bd3f5b9ee894518f9d9ed1019fd8b7014340beca512dfb76d5632ec9/hdf5plugin-5.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_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/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2b/d3/85feeba1d097b81a44bcffa6a0beab7b4dfffe78e82fc54978d3ac380736/identify-2.6.10-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/ce/461b60a3ee109518c055953729bf9ed089a04db895d47e95444071dcdef2/identify-2.6.13-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/79/9d/0fb148dc4d6fa4a7dd1d8378168d9b4cd8d4560a6fbf6f0121c5fc34eb68/importlib_metadata-8.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b2/9d/4ff2adf55d1b6e3777b0303fdbe5b723f76e46cba4a53a32fe82260d2077/ipython-9.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/08/2a/5628a99d04acb2d2f2e749cdf4ea571d2575e898df0528a090948018b726/ipython-9.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl @@ -173,7 +172,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/cb/c1/fb2c66d92b5d0167c57042b784456ee3f8531a997726c88cf6f012a22da6/json_merge_patch-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d2/1b/656f5a357de7135a3777bd735cc7c9b8f23b4d37465505bd0eaf4be9befe/llvmlite-0.44.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl @@ -183,62 +182,63 @@ environments: - pypi: https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2d/7b/2c1d74ca6c94f70a1add74a8393a0138172207dc5de6fc6269483519d048/msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/20/22/2ebae7ae43cd8f2debc35c631172ddf14e2a87ffcc04cf43ff9df9fff0d3/msgpack-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.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/b5/07/c64b0c8416f604f6990da5d1fa97c9de1278a4eec1efcc63b71053b4f0c0/ndindex-1.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a6/91/86a6eac449ddfae239e93ffc1918cf33fd9bab35c04d1e963b311e347a73/netifaces-0.11.0.tar.gz - - pypi: https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0d/e0/5ea04e7ad2c39288c0f0f9e8d47638ad70f28e275d092733b5817cf243c9/numba-0.61.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/fa/91/d96999b41e3146b6c0ce6bddc5ad85803cb4d743c95394562c2a4bb8cded/numcodecs-0.15.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/55/7f/0ab8db32ef9b51c60f7b759c2b155e1edcb08febb508c22a9d04b19ec735/numcodecs-0.16.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b9/7d/4911f40d3610fc5557029f0d1f20ef9f571488319567ac4d8ee6d0978ee6/numexpr-2.11.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/aa/fc/ebfd32c3e124e6a1043e19c0ab0769818aa69050ce5589b63d05ff185526/numpy-2.2.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.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/12/f2/89ea3361a305466bc6460a532188830351220b5f0851a5fa133155c16eca/opentelemetry_api-1.32.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f2/5e/f5ded172af40b7e7ae60697a2b2096cec53e45344edce41e91a74425473c/ophyd-1.10.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/85/38/6b455b1d6f0b9aa3db4e8888b797f77d856a8350d836c66a43d1078573b2/ophyd_async-0.10.0a4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6d/4c/2bda09855c6b5f2c055034c9eda1529967b042ff8d81a05005115c4e6772/orjson-3.10.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/bb/ee/6b08dde0a022c463b88f55ae81149584b125a42183407dc1045c486cc870/opentelemetry_api-1.36.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fa/b3/1e63820f1a4df7854e33f0c3c5fa6be758c7fb7ea007ae9c8b9b35f48690/ophyd-1.10.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c8/cb/75a17d45ae070fa4af0b6bf79fb820b9d9137bf09f485a6728f71e4a062e/ophyd_async-0.12.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d0/b4/f98355eff0bd1a38454209bbc73372ce351ba29933cb3e2eba16c04b9448/orjson-3.11.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8f/52/0634adaace9be2d8cac9ef78f05c47f3a675882e068438b9d7ec7ef0c13f/pandas-2.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/b7/16/bd2f5904557265882108dc2e04f18abc05ab0c2b7082ae9430091daf1d5c/Pint-0.24.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/76/cc/c528311d798e22ec884b816e8aa2989e0f1f28cdc8e5969e2be5f10bce85/pint-0.25-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/ae/ec06af4fe3ee72d16973474f122541746196aaa16cea6f66d18b963c6177/prometheus_client-0.22.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/be/27/43a47fa0ff9053ab5203bb3faeec435d43c0d8bfa40179bfd076cdbd4e1c/pyarrow-20.0.0-cp313-cp313-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/89/4b/7782438b551dbb0468892a276b8c789b8bbdb25ea5c5eb27faadd753e037/pyarrow-21.0.0-cp313-cp313-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.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/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/49/b6/b04e5c2f41a5ccad74a1a4759da41adb20b4bc9d59a5e08d29ba60084d07/pyright-1.1.403-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/30/05/ce271016e351fddc8399e546f6e23761967ee09c8c568bbfbecb0c150171/pytest_asyncio-1.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/30/89aa7f7d7a875bbb9a577d4b1dc5a3e404e3d2ae2657354808e905e358e0/pyright-1.1.404-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/9d/bf86eddabf8c6c9cb1ea9a869d6873b46f105a5d292d3a6f7071f5b07935/pytest_asyncio-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.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/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e8/02/89e2ed7e85db6c93dfa9e8f691c5087df4e3551ab39081a4d7c6d1f90e05/redis-6.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/05/7c/8abb70f9017a231c6c961a8941403ed6557664c0913e1bf413cbdc039e75/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/03/36/0a14aebbaa26fe7fab4780c76f2239e76cc95a0090bdb25e31d95c492fcd/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_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/04/9e/91fe10ea09e2383f0d0531cf3da765fa01b3b9918e85df92081813259cf4/ruamel.yaml-0.18.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/e5/f2a0621f1781b76a38194acae72f01e37b1941470407345b6e8653ad7640/ruamel.yaml-0.18.15-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1f/8f/ecfbe2123ade605c49ef769788f79c38ddb1c8fa81e01f4dbf5cf1a44b16/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/0e/2c/1e364cc92970075d7d04c69c928430b23e43a433f044474f57e425cbed37/ruff-0.11.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/80/22/3c6cef96627f89b344c933781ed38329bfb87737aa438f15da95907cbfd5/ruff-0.12.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b9/36/c101788fad13e8ea65c5b3d3dee8ff996500800cd554ae6ff72143690247/setuptools_dso-2.12.2-py2.py3-none-any.whl @@ -246,33 +246,32 @@ environments: - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8d/fd/42a1720542199ae6ff0f9c37bbd55dd3033ddd7bbe00d68cde09d6824887/sparse-0.17.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5e/51/5ba9ea3246ea068630acf35a6ba0d181e99f1af1afd17e159eac7e8bc2b8/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/dc/29/11ae2c2b981de60187f7cbc84277d9d21f101093d1b2e945c63774477aba/sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/14/ba/d03f7ee711391af1d5f4dd7c44f8abdd06bce247028af2441ba8f6ff329b/stamina-25.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ce/fd/901cfa59aaa5b30a99e16876f11abe38b59a1a2c51ffb3d7142bb6089069/starlette-0.47.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3a/d8/1ba8f32bfc9cb69e37edeca93738e883f478fbe84ae401f72c0d8d507841/tifffile-2025.6.11-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/37/63/383de8d9400a52d55683940387b160efb4f6b35bfb7645a443008780e2a9/tiled-0.1.0b29-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/56/b3/23eec760215910609914dd99aba23ce1c72a3bcbe046ee44f45adf740452/tifffile-2025.8.28-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/a1/815d1083065462f4947759bbb6fe60456bd9eb944cb862c6ed7dba7fe6f2/tiled-0.1.0b39-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/03/98/eb27cc78ad3af8e302c9d8ff4977f5026676e130d28dd7578132a457170c/toolz-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/e8/b3d537470e8404659a6335e7af868e90657efb73916ef31ddf3d8b9cb237/typer-0.17.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/4c/ed/3cfeb48175f0671ec430ede81f628f9fb2b1084c9064ca67ebe8c0ed6a05/virtualenv-20.30.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/25/0d/4d564798a49bf5482a4fa9416dea6b6c0733a3b5700cb8a5a503c4b15853/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/82/8a/6b50c1dd2260d407c1a499d47cf829f59f07007e0dcebafdabb24d1d26a5/xarray-2025.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5e/d8/9ffd8c237b3559945bb52103cf0eed64ea098f7b7f573f8d2962ef27b4b2/zarr-2.18.7-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/18/89ac62eac46b69948bf35fcd90d37103f38722968e2981f752d69081ec4d/zstandard-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: . + - pypi: https://files.pythonhosted.org/packages/e6/c8/0f8db9d9478de8d70cbcae2056588401e26168e269d6d9919bf2ecb01f78/xarray-2025.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/a3/d3d4fd394a10b1256f9dccb2fe0ddd125fc575d7c437b1c70df050f14176/zarr-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d9/3d/02aba892327a67ead8cba160ee835cfa1fc292a9dcb763639e30c07da58b/zstandard-0.24.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: ./ docs: channels: - url: https://conda.anaconda.org/conda-forge/ @@ -283,83 +282,83 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.1.31-hbd8a1cb_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.2.0-h767d61c_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.2.0-h69a702a_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.2.0-h767d61c_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-h4bc722e_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.49.1-hee588c1_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.0-h7b32b05_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.3-hf636f53_101_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-7_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.5-hec9711d_102_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - pypi: https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-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/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fc/30/f7da949893bf0d8525acbaee256cf55430332852225f17e18a7f8783dd6f/bluesky-1.14.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/04/eb/f4151e0c7377a6e08a38108609ba5cede57986802757848688aeedd1b9e8/beautifulsoup4-4.13.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/41/497f55b454aa64e2cb3f27990e5cb76c64457d54365b07d6c125bdac7b38/bluesky-1.14.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-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/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b1/f2/47e3f0e06c3bc3834db2a4ed01641e1ce493108461b9540dd5d2fb60d334/event_model-1.22.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/32/e31e3363bf48ad2ba80b644b01ad9676ce154f1b755950de81eb4ed5b6bd/event_model-1.23.1-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/fe/5e/3be305568fe5f34448807976dc82fc151d76c3e0e03958f34770286278c1/flexparser-0.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/27/48/e791a7ed487dbb9729ef32bb5d1af16693d8925f4366befef54119b2e576/furo-2024.8.6-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/41/bc2df86b72965775f6d621e0ee269a5f3ac23e8f870abf519de9c7d93b4d/h5py-3.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/3a/34/2b07b72bee02a63241d654f5d8af87a2de977c59638eec41ca356ab915cd/furo-2025.7.19-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/ec/86f59025306dcc6deee5fda54d980d077075b8d9889aac80f158bd585f1b/h5py-3.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_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/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/79/9d/0fb148dc4d6fa4a7dd1d8378168d9b4cd8d4560a6fbf6f0121c5fc34eb68/importlib_metadata-8.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/97/12/a1f2f4fdc6b7159c0d12249456f9fe454665b6126e98dbee9f2bd3cf735c/lz4-4.4.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/a7/f7/7782a043553ee469c1ff49cfa1cdace2d6bf99a1f333cf38676b3ddf30da/mdit_py_plugins-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2d/7b/2c1d74ca6c94f70a1add74a8393a0138172207dc5de6fc6269483519d048/msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/20/22/2ebae7ae43cd8f2debc35c631172ddf14e2a87ffcc04cf43ff9df9fff0d3/msgpack-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.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/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/fc/ebfd32c3e124e6a1043e19c0ab0769818aa69050ce5589b63d05ff185526/numpy-2.2.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/12/f2/89ea3361a305466bc6460a532188830351220b5f0851a5fa133155c16eca/opentelemetry_api-1.32.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f2/5e/f5ded172af40b7e7ae60697a2b2096cec53e45344edce41e91a74425473c/ophyd-1.10.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/85/38/6b455b1d6f0b9aa3db4e8888b797f77d856a8350d836c66a43d1078573b2/ophyd_async-0.10.0a4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/bb/ee/6b08dde0a022c463b88f55ae81149584b125a42183407dc1045c486cc870/opentelemetry_api-1.36.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fa/b3/1e63820f1a4df7854e33f0c3c5fa6be758c7fb7ea007ae9c8b9b35f48690/ophyd-1.10.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c8/cb/75a17d45ae070fa4af0b6bf79fb820b9d9137bf09f485a6728f71e4a062e/ophyd_async-0.12.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b7/16/bd2f5904557265882108dc2e04f18abc05ab0c2b7082ae9430091daf1d5c/Pint-0.24.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/76/cc/c528311d798e22ec884b816e8aa2989e0f1f28cdc8e5969e2be5f10bce85/pint-0.25-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.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/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/05/7c/8abb70f9017a231c6c961a8941403ed6557664c0913e1bf413cbdc039e75/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/04/9e/91fe10ea09e2383f0d0531cf3da765fa01b3b9918e85df92081813259cf4/ruamel.yaml-0.18.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/03/36/0a14aebbaa26fe7fab4780c76f2239e76cc95a0090bdb25e31d95c492fcd/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/d1/e5/f2a0621f1781b76a38194acae72f01e37b1941470407345b6e8653ad7640/ruamel.yaml-0.18.15-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1f/8f/ecfbe2123ade605c49ef769788f79c38ddb1c8fa81e01f4dbf5cf1a44b16/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/14/2f/bc5bed0677ae00b9ca7919968ea675e2f696b6b20f1648262f26a7a6c6b4/sphinx_autodoc_typehints-3.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/c7/8aab362e86cbf887e58be749a78d20ad743e1eb2c73c2b13d4761f39a104/sphinx_autodoc_typehints-3.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3c/dd/018ce05c532a22007ac58d4f45232514cd9d6dd0ee1dc374e309db830983/sphinx_basic_ng-1.0.0b2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl @@ -368,14 +367,15 @@ environments: - 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/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-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/14/ba/d03f7ee711391af1d5f4dd7c44f8abdd06bce247028af2441ba8f6ff329b/stamina-25.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/03/98/eb27cc78ad3af8e302c9d8ff4977f5026676e130d28dd7578132a457170c/toolz-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl - - pypi: . + - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl + - pypi: ./ test: channels: - url: https://conda.anaconda.org/conda-forge/ @@ -386,66 +386,65 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.1.31-hbd8a1cb_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.2.0-h767d61c_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.2.0-h69a702a_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.2.0-h767d61c_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-h4bc722e_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.49.1-hee588c1_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.0-h7b32b05_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.3-hf636f53_101_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-7_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.5-hec9711d_102_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - pypi: https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dd/e2/88e425adac5ad887a087c38d04fe2030010572a3e0e627f8a6e8c33eeda8/alembic-1.16.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/39/4a/4c61d4c84cfd9befb6fa08a702535b27b21fff08c946bc2f6139decbf7f7/alembic-1.16.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2d/6a/885bc91484e1aa8f618f6f0228d76d0e67000b0fdd6090673b777e311913/asciitree-0.3.3.tar.gz + - pypi: https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/ab/6936e2663c47a926e0659437b9333ad87d1ff49b1375d239026e0a268eba/asgi_correlation_id-4.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fc/30/f7da949893bf0d8525acbaee256cf55430332852225f17e18a7f8783dd6f/bluesky-1.14.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/00/f0/2ef431fe4141f5e334759d73e81120492b23b2824336883a91ac04ba710b/cachetools-6.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/41/497f55b454aa64e2cb3f27990e5cb76c64457d54365b07d6c125bdac7b38/bluesky-1.14.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6c/56/3124f61d37a7a4e7cc96afc5492c78ba0cb551151e530b54669ddd1436ef/cachetools-6.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/54/346f681c24a9c3a08e2e74dcee2555ccd1081705b46f791f7b228e177d06/canonicaljson-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e9/ad/0ce6cef4823d6c44b87e9b26b2cb284f6e25143ea0ab8215ac836bda825e/caproto-1.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/e8/64c37fadfc2816a7701fa8a6ed8d87327c7d54eacfbfb6edab14a2f2be75/cloudpickle-3.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-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/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/ec/ef/3333295ed58d900a13c92806b67e62f27876845a9a908c939f040887cca9/cryptography-45.0.5-cp311-abi3-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/73/dd/508420fb47d09d904d962f123221bc249f64b5e56aa93d5f5f7603be475f/coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/01/cf/32f019be5de9f6e180926a50ee5f08648e686c7d9a59f2c5d0806a77b1c7/crc32c-2.7.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/96/b8/bca71059e79a0bb2f8e4ec61d9c205fbe97876318566cde3b5092529faa9/cryptography-45.0.7-cp311-abi3-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/38/30/53b0844a7a4c6b041b111b24ca15cc9b8661a86fe1f6aaeb2d0d7f0fb1f2/dask-2025.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/f9/3e04725358c17329652da8c1b2dbd88de723f3dc78bf52ca6d28d52c9279/dask-2025.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/11/79/479e2194c9096b92aecdf33634ae948d2be306c6011673e98ee1917f32c2/dpkt-1.9.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/a3/460c57f094a4a165c84a1341c373b0a4f5ec6ac244b998d5021aade89b77/ecdsa-0.19.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/35/a8/365059bbcd4572cbc41de17fd5b682be5868b218c3c5479071865cab9078/entrypoints-0.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b1/f2/47e3f0e06c3bc3834db2a4ed01641e1ce493108461b9540dd5d2fb60d334/event_model-1.22.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/53/50/b1222562c6d270fea83e9c9075b8e8600b8479150a18e4516a6138b980d1/fastapi-0.115.14-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/61/bf/fd60001b3abc5222d8eaa4a204cd8c0ae78e75adc688f33ce4bf25b7fafa/fasteners-0.19-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/32/e31e3363bf48ad2ba80b644b01ad9676ce154f1b755950de81eb4ed5b6bd/event_model-1.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/47/d63c60f59a59467fda0f93f46335c9d18526d7071f025cb5b89d5353ea42/fastapi-0.116.1-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/fe/5e/3be305568fe5f34448807976dc82fc151d76c3e0e03958f34770286278c1/flexparser-0.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bb/61/78c7b3851add1481b048b5fdc29067397a1784e2910592bc81bb3f608635/fsspec-2025.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/26/f2/ad51331a157c7015c675702e2d5230c243695c788f8f75feba1af32b3617/greenlet-3.2.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2f/e0/014d5d9d7a4564cf1c40b5039bc882db69fd881111e03ab3657ac0b218e2/fsspec-2025.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/41/bc2df86b72965775f6d621e0ee269a5f3ac23e8f870abf519de9c7d93b4d/h5py-3.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e7/ec/86f59025306dcc6deee5fda54d980d077075b8d9889aac80f158bd585f1b/h5py-3.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_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/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/79/9d/0fb148dc4d6fa4a7dd1d8378168d9b4cd8d4560a6fbf6f0121c5fc34eb68/importlib_metadata-8.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl @@ -453,7 +452,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/cb/c1/fb2c66d92b5d0167c57042b784456ee3f8531a997726c88cf6f012a22da6/json_merge_patch-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/97/12/a1f2f4fdc6b7159c0d12249456f9fe454665b6126e98dbee9f2bd3cf735c/lz4-4.4.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl @@ -461,63 +460,65 @@ environments: - pypi: https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2d/7b/2c1d74ca6c94f70a1add74a8393a0138172207dc5de6fc6269483519d048/msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/20/22/2ebae7ae43cd8f2debc35c631172ddf14e2a87ffcc04cf43ff9df9fff0d3/msgpack-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.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/a6/91/86a6eac449ddfae239e93ffc1918cf33fd9bab35c04d1e963b311e347a73/netifaces-0.11.0.tar.gz - - pypi: https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/91/d96999b41e3146b6c0ce6bddc5ad85803cb4d743c95394562c2a4bb8cded/numcodecs-0.15.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/aa/fc/ebfd32c3e124e6a1043e19c0ab0769818aa69050ce5589b63d05ff185526/numpy-2.2.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/12/f2/89ea3361a305466bc6460a532188830351220b5f0851a5fa133155c16eca/opentelemetry_api-1.32.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f2/5e/f5ded172af40b7e7ae60697a2b2096cec53e45344edce41e91a74425473c/ophyd-1.10.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/85/38/6b455b1d6f0b9aa3db4e8888b797f77d856a8350d836c66a43d1078573b2/ophyd_async-0.10.0a4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6d/4c/2bda09855c6b5f2c055034c9eda1529967b042ff8d81a05005115c4e6772/orjson-3.10.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/55/7f/0ab8db32ef9b51c60f7b759c2b155e1edcb08febb508c22a9d04b19ec735/numcodecs-0.16.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/bb/ee/6b08dde0a022c463b88f55ae81149584b125a42183407dc1045c486cc870/opentelemetry_api-1.36.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fa/b3/1e63820f1a4df7854e33f0c3c5fa6be758c7fb7ea007ae9c8b9b35f48690/ophyd-1.10.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c8/cb/75a17d45ae070fa4af0b6bf79fb820b9d9137bf09f485a6728f71e4a062e/ophyd_async-0.12.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d0/b4/f98355eff0bd1a38454209bbc73372ce351ba29933cb3e2eba16c04b9448/orjson-3.11.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b7/16/bd2f5904557265882108dc2e04f18abc05ab0c2b7082ae9430091daf1d5c/Pint-0.24.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/76/cc/c528311d798e22ec884b816e8aa2989e0f1f28cdc8e5969e2be5f10bce85/pint-0.25-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/ae/ec06af4fe3ee72d16973474f122541746196aaa16cea6f66d18b963c6177/prometheus_client-0.22.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.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/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/30/05/ce271016e351fddc8399e546f6e23761967ee09c8c568bbfbecb0c150171/pytest_asyncio-1.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/9d/bf86eddabf8c6c9cb1ea9a869d6873b46f105a5d292d3a6f7071f5b07935/pytest_asyncio-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.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/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e8/02/89e2ed7e85db6c93dfa9e8f691c5087df4e3551ab39081a4d7c6d1f90e05/redis-6.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/05/7c/8abb70f9017a231c6c961a8941403ed6557664c0913e1bf413cbdc039e75/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/03/36/0a14aebbaa26fe7fab4780c76f2239e76cc95a0090bdb25e31d95c492fcd/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_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/04/9e/91fe10ea09e2383f0d0531cf3da765fa01b3b9918e85df92081813259cf4/ruamel.yaml-0.18.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/e5/f2a0621f1781b76a38194acae72f01e37b1941470407345b6e8653ad7640/ruamel.yaml-0.18.15-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1f/8f/ecfbe2123ade605c49ef769788f79c38ddb1c8fa81e01f4dbf5cf1a44b16/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5e/51/5ba9ea3246ea068630acf35a6ba0d181e99f1af1afd17e159eac7e8bc2b8/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/37/63/383de8d9400a52d55683940387b160efb4f6b35bfb7645a443008780e2a9/tiled-0.1.0b29-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/29/11ae2c2b981de60187f7cbc84277d9d21f101093d1b2e945c63774477aba/sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/14/ba/d03f7ee711391af1d5f4dd7c44f8abdd06bce247028af2441ba8f6ff329b/stamina-25.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ce/fd/901cfa59aaa5b30a99e16876f11abe38b59a1a2c51ffb3d7142bb6089069/starlette-0.47.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/a1/815d1083065462f4947759bbb6fe60456bd9eb944cb862c6ed7dba7fe6f2/tiled-0.1.0b39-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/03/98/eb27cc78ad3af8e302c9d8ff4977f5026676e130d28dd7578132a457170c/toolz-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/e8/b3d537470e8404659a6335e7af868e90657efb73916ef31ddf3d8b9cb237/typer-0.17.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/25/0d/4d564798a49bf5482a4fa9416dea6b6c0733a3b5700cb8a5a503c4b15853/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/5e/d8/9ffd8c237b3559945bb52103cf0eed64ea098f7b7f573f8d2962ef27b4b2/zarr-2.18.7-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl - - pypi: . + - pypi: https://files.pythonhosted.org/packages/e0/a3/d3d4fd394a10b1256f9dccb2fe0ddd125fc575d7c437b1c70df050f14176/zarr-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl + - pypi: ./ packages: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 @@ -540,23 +541,39 @@ packages: purls: [] size: 23621 timestamp: 1650670423406 -- pypi: https://files.pythonhosted.org/packages/d3/49/b5e260deff3d218a17fe23a1313bb3c033d846bf74505c297f74d2c8abfe/adbc_driver_manager-1.6.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl + name: accessible-pygments + version: 0.0.5 + sha256: 88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7 + requires_dist: + - pygments>=1.5 + - pillow ; extra == 'dev' + - pkginfo>=1.10 ; extra == 'dev' + - playwright ; extra == 'dev' + - pre-commit ; extra == 'dev' + - setuptools ; extra == 'dev' + - twine>=5.0 ; extra == 'dev' + - hypothesis ; extra == 'tests' + - pytest ; extra == 'tests' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/a2/22/44738b41bb5ca30f94b5f4c00c71c20be86d7eb4ddc389d4cf3c7b8b69ef/adbc_driver_manager-1.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl name: adbc-driver-manager - version: 1.6.0 - sha256: 41972465fa4db46bf151cc37000d0bd29c87c2eabbc81f502f0b6932c235f213 + version: 1.7.0 + sha256: 98db0f5d0aa1635475f63700a7b6f677390beb59c69c7ba9d388bc8ce3779388 requires_dist: - typing-extensions - pandas ; extra == 'dbapi' - pyarrow>=14.0.1 ; extra == 'dbapi' - duckdb ; extra == 'test' - pandas ; extra == 'test' + - polars ; extra == 'test' - pyarrow>=14.0.1 ; extra == 'test' - pytest ; extra == 'test' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/c1/45/2d3514fd2bf7ee7e1e28c45fdb70f2a27db2f8cc8af7e7c28f846dd7b99d/adbc_driver_postgresql-1.6.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/58/15/86561628738161017273d9a689e9405e4ea9a9d41a70fd2460dbc5d646ae/adbc_driver_postgresql-1.7.0-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl name: adbc-driver-postgresql - version: 1.6.0 - sha256: c3d09a688fb95bbe5431b488431145d77b3e20f8adbc413b1c2530b6623a9a2a + version: 1.7.0 + sha256: da257df9e168e05f3a13b9da974d58b5580c70dc881f9f100c80f789e0cb336b requires_dist: - adbc-driver-manager - importlib-resources>=1.3 @@ -567,10 +584,10 @@ packages: - pyarrow>=14.0.1 ; extra == 'test' - pytest ; extra == 'test' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/92/ec/ce41f3c84b5b4f762e4de9072574714c623e959924bf1bbaf1548612cb6e/adbc_driver_sqlite-1.6.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/93/1f/618d88542ca66baf6bc25a3e5ecbd698eff31b12b2ab2a590bae8d9d8c83/adbc_driver_sqlite-1.7.0-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl name: adbc-driver-sqlite - version: 1.6.0 - sha256: 922a561c84c53f8fac42f1d636dfb915a265de3ebd39c35cf0d1cbe58d668616 + version: 1.7.0 + sha256: 2b3aba1b27ec9cc5761cfe4a870839a6e313e6f580f9f673fbec72299b76fa7d requires_dist: - adbc-driver-manager - importlib-resources>=1.3 @@ -633,10 +650,10 @@ packages: version: 1.0.0 sha256: fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/dd/e2/88e425adac5ad887a087c38d04fe2030010572a3e0e627f8a6e8c33eeda8/alembic-1.16.2-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/39/4a/4c61d4c84cfd9befb6fa08a702535b27b21fff08c946bc2f6139decbf7f7/alembic-1.16.5-py3-none-any.whl name: alembic - version: 1.16.2 - sha256: 5f42e9bd0afdbd1d5e3ad856c01754530367debdebf21ed6894e34af52b3bb03 + version: 1.16.5 + sha256: e845dfe090c5ffa7b92593ae6687c5cb1a101e91fa53868497dbd79847f9dbe3 requires_dist: - sqlalchemy>=1.4.0 - mako @@ -651,35 +668,17 @@ packages: requires_dist: - typing-extensions>=4.0.0 ; python_full_version < '3.9' requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl name: anyio - version: 4.9.0 - sha256: 9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c + version: 4.10.0 + sha256: 60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1 requires_dist: - exceptiongroup>=1.0.2 ; python_full_version < '3.11' - idna>=2.8 - sniffio>=1.1 - typing-extensions>=4.5 ; python_full_version < '3.13' - trio>=0.26.1 ; extra == 'trio' - - anyio[trio] ; extra == 'test' - - blockbuster>=1.5.23 ; extra == 'test' - - coverage[toml]>=7 ; extra == 'test' - - exceptiongroup>=1.2.0 ; extra == 'test' - - hypothesis>=4.0 ; extra == 'test' - - psutil>=5.9 ; extra == 'test' - - pytest>=7.0 ; extra == 'test' - - trustme ; extra == 'test' - - truststore>=0.9.1 ; python_full_version >= '3.10' and extra == 'test' - - uvloop>=0.21 ; python_full_version < '3.14' and platform_python_implementation == 'CPython' and sys_platform != 'win32' and extra == 'test' - - packaging ; extra == 'doc' - - sphinx~=8.2 ; extra == 'doc' - - sphinx-rtd-theme ; extra == 'doc' - - sphinx-autodoc-typehints>=1.2.0 ; extra == 'doc' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/2d/6a/885bc91484e1aa8f618f6f0228d76d0e67000b0fdd6090673b777e311913/asciitree-0.3.3.tar.gz - name: asciitree - version: 0.3.3 - sha256: 4aa4b9b649f85e3fcb343363d97564aa1fb62e249677f2e18a96765145cc0f6e - pypi: https://files.pythonhosted.org/packages/d9/ab/6936e2663c47a926e0659437b9333ad87d1ff49b1375d239026e0a268eba/asgi_correlation_id-4.3.4-py3-none-any.whl name: asgi-correlation-id version: 4.3.4 @@ -765,22 +764,22 @@ packages: - mypy>=1.11.1 ; python_full_version >= '3.10' and platform_python_implementation == 'CPython' and extra == 'tests-mypy' - pytest-mypy-plugins ; python_full_version >= '3.10' and platform_python_implementation == 'CPython' and extra == 'tests-mypy' requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/d2/af/92435ec3cfe35bd738a3ba2332c8cbdb50f0cc66d1f9943a1a457990fcd6/awkward-2.8.4-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/57/19/c547e21267fb9bbeb8acde6996fcc21ff4a76eed182e9a9a441480992f57/awkward-2.8.7-py3-none-any.whl name: awkward - version: 2.8.4 - sha256: 6889e632497be712c2515a15947a352ed3752729c1311802722b432f7db95a5d + version: 2.8.7 + sha256: 34f516dc4916efc451552a185363e7a7cd2cf33a1ae62c96f2ff6f5cc9a02919 requires_dist: - - awkward-cpp==46 + - awkward-cpp==48 - fsspec>=2022.11.0 - importlib-metadata>=4.13.0 ; python_full_version < '3.12' - numpy>=1.18.0 - packaging - typing-extensions>=4.1.0 ; python_full_version < '3.11' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/2b/cf/9424352bd9568bd56eb88e5fc8400a6c5bd3c827fdeeae202fcaaf3c1910/awkward_cpp-46-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/1d/a9/87c1a3c29f2378074755f254ed0fe478f0c5ecf2eea94f450d6b0bcee33f/awkward_cpp-48-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl name: awkward-cpp - version: '46' - sha256: 53180c40c05ffb3f1671a9cde59dde34b0cc49b6089aa6774a4067f14dbf08d2 + version: '48' + sha256: d6d5058c8bd3ea2a2855e225430fe690f23bb0fb66b96a4581bd28bef6e73233 requires_dist: - numpy>=1.18.0 requires_python: '>=3.9' @@ -799,10 +798,10 @@ packages: - pytz ; extra == 'dev' - setuptools ; extra == 'dev' requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/04/eb/f4151e0c7377a6e08a38108609ba5cede57986802757848688aeedd1b9e8/beautifulsoup4-4.13.5-py3-none-any.whl name: beautifulsoup4 - version: 4.13.4 - sha256: 9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b + version: 4.13.5 + sha256: 642085eaa22233aceadff9c69651bc51e8bf3f874fb6d7104ece2beb24b47c4a requires_dist: - soupsieve>1.2 - typing-extensions>=4.0.0 @@ -812,10 +811,10 @@ packages: - html5lib ; extra == 'html5lib' - lxml ; extra == 'lxml' requires_python: '>=3.7.0' -- pypi: https://files.pythonhosted.org/packages/c9/38/8815054e054e45fda3c9d1e25ac77e6387d10d8e33c64bcf79f432b3c4fa/blosc2-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/25/70/020cc09340973f361fcfbcfa2933fdb6301170edbe17215e24a5351d4c72/blosc2-3.7.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl name: blosc2 - version: 3.5.1 - sha256: f88cfa1f1d64d77278c7fae0ab8c42291b7b2e8f36fcee154a0c55388e697fe9 + version: 3.7.2 + sha256: 78de4de35bb883fa627b2668265ff2f92586365c5a68da9f450115d5d1afa948 requires_dist: - numpy>=1.26 - ndindex @@ -846,12 +845,15 @@ packages: - myst-parser ; extra == 'doc' - sphinx-paramlinks ; extra == 'doc' - nbsphinx ; extra == 'doc' - - sphinx-panels ; extra == 'doc' + - ipykernel ; extra == 'doc' + - sphinx-design ; extra == 'doc' + - furo ; extra == 'doc' + - numba ; extra == 'doc' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/fc/30/f7da949893bf0d8525acbaee256cf55430332852225f17e18a7f8783dd6f/bluesky-1.14.1-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/c1/41/497f55b454aa64e2cb3f27990e5cb76c64457d54365b07d6c125bdac7b38/bluesky-1.14.4-py3-none-any.whl name: bluesky - version: 1.14.1 - sha256: e496fa9e6ef055eb0cb5ee4580abeb7e2b2bcc1e18c5ba2904819a7a42e7ff0d + version: 1.14.4 + sha256: 059c461d1db3032b0042878d5572cc33c33ff825c11ca414cf7f3feb926b5efb requires_dist: - cycler - event-model>=1.19.8 @@ -906,6 +908,7 @@ packages: - sphinx-autobuild ; extra == 'dev' - sphinx-copybutton ; extra == 'dev' - sphinx-design ; extra == 'dev' + - sphinxcontrib-mermaid ; extra == 'dev' - sphinx-rtd-theme ; extra == 'dev' - streamz ; extra == 'dev' - suitcase-jsonl ; extra == 'dev' @@ -943,19 +946,19 @@ packages: purls: [] size: 252783 timestamp: 1720974456583 -- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.1.31-hbd8a1cb_1.conda - sha256: 43878eddf8eb46e3ba7fcbe77a2f8d00aab9a66d9ff63bc4d072b7af17481197 - md5: e74273d9fc5ab633d613cde474b55157 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda + sha256: 837b795a2bb39b75694ba910c13c15fa4998d4bb2a622c214a6a5174b2ae53d1 + md5: 74784ee3d225fc3dca89edb635b4e5cc depends: - __unix license: ISC purls: [] - size: 158834 - timestamp: 1745260472197 -- pypi: https://files.pythonhosted.org/packages/00/f0/2ef431fe4141f5e334759d73e81120492b23b2824336883a91ac04ba710b/cachetools-6.1.0-py3-none-any.whl + size: 154402 + timestamp: 1754210968730 +- pypi: https://files.pythonhosted.org/packages/6c/56/3124f61d37a7a4e7cc96afc5492c78ba0cb551151e530b54669ddd1436ef/cachetools-6.2.0-py3-none-any.whl name: cachetools - version: 6.1.0 - sha256: 1c7bb3cf9193deaf3508b7c5f2a79986c13ea38965c5adcff1f84519cf39163e + version: 6.2.0 + sha256: 1c76a8960c0041fcc21097e357f882197c79da0dbff766e7317890a65d7d8ba6 requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/e5/54/346f681c24a9c3a08e2e74dcee2555ccd1081705b46f791f7b228e177d06/canonicaljson-2.0.0-py3-none-any.whl name: canonicaljson @@ -995,9 +998,9 @@ packages: - trio>=0.18.0 ; extra == 'test' - trio>=0.12.1 ; extra == 'test' requires_python: '>=3.8' -- pypi: . +- pypi: ./ name: cditools - version: 0.1.dev78+geefc1ac.d20250722 + version: 0.1.dev80+gefdaff665 sha256: c080375c6ec2ef7fc203adf19c93127ab7b82aa23bc5db1c6ddc30349b52b882 requires_dist: - ophyd @@ -1028,11 +1031,11 @@ packages: - furo>=2023.8.17 ; extra == 'docs' requires_python: '>=3.9' editable: true -- pypi: https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl name: certifi - version: 2025.1.31 - sha256: ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe - requires_python: '>=3.6' + version: 2025.8.3 + sha256: f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5 + requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl name: cffi version: 1.17.1 @@ -1045,10 +1048,10 @@ packages: version: 3.4.0 sha256: b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl name: charset-normalizer - version: 3.4.1 - sha256: 955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11 + version: 3.4.3 + sha256: 416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl name: click @@ -1086,23 +1089,28 @@ packages: - cloudpickle ; extra == 'full' - lz4 ; extra == 'lz4' requires_python: '>=3.6' -- pypi: https://files.pythonhosted.org/packages/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/73/dd/508420fb47d09d904d962f123221bc249f64b5e56aa93d5f5f7603be475f/coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl name: coverage - version: 7.8.0 - sha256: 5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008 + version: 7.10.6 + sha256: 0f3f56e4cb573755e96a16501a98bf211f100463d70275759e73f3cbc00d4f27 requires_dist: - tomli ; python_full_version <= '3.11' and extra == 'toml' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/ec/ef/3333295ed58d900a13c92806b67e62f27876845a9a908c939f040887cca9/cryptography-45.0.5-cp311-abi3-manylinux_2_28_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/01/cf/32f019be5de9f6e180926a50ee5f08648e686c7d9a59f2c5d0806a77b1c7/crc32c-2.7.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: crc32c + version: 2.7.1 + sha256: 724d5ff4d29ff093a983ae656be3307093706d850ea2a233bf29fcacc335d945 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/96/b8/bca71059e79a0bb2f8e4ec61d9c205fbe97876318566cde3b5092529faa9/cryptography-45.0.7-cp311-abi3-manylinux_2_28_x86_64.whl name: cryptography - version: 45.0.5 - sha256: dd420e577921c8c2d31289536c386aaa30140b473835e97f83bc71ea9d2baf2d + version: 45.0.7 + sha256: 16ede8a4f7929b4b7ff3642eba2bf79aa1d71f24ab6ee443935c0d269b6bc513 requires_dist: - cffi>=1.14 ; platform_python_implementation != 'PyPy' - bcrypt>=3.1.5 ; extra == 'ssh' - nox>=2024.4.15 ; extra == 'nox' - nox[uv]>=2024.3.2 ; python_full_version >= '3.8' and extra == 'nox' - - cryptography-vectors==45.0.5 ; extra == 'test' + - cryptography-vectors==45.0.7 ; extra == 'test' - pytest>=7.4.0 ; extra == 'test' - pytest-benchmark>=4.0 ; extra == 'test' - pytest-cov>=2.10.1 ; extra == 'test' @@ -1135,10 +1143,10 @@ packages: - pytest-cov ; extra == 'tests' - pytest-xdist ; extra == 'tests' requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/38/30/53b0844a7a4c6b041b111b24ca15cc9b8661a86fe1f6aaeb2d0d7f0fb1f2/dask-2025.5.1-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/b3/f9/3e04725358c17329652da8c1b2dbd88de723f3dc78bf52ca6d28d52c9279/dask-2025.7.0-py3-none-any.whl name: dask - version: 2025.5.1 - sha256: 3b85fdaa5f6f989dde49da6008415b1ae996985ebdfb1e40de2c997d9010371d + version: 2025.7.0 + sha256: 073c29c4e99c2400a39a8a67874f35c1d15bf7af9ae3d0c30af3c7c1199f24ae requires_dist: - click>=8.1 - cloudpickle>=3.0.0 @@ -1152,7 +1160,7 @@ packages: - dask[array] ; extra == 'dataframe' - pandas>=2.0 ; extra == 'dataframe' - pyarrow>=14.0.1 ; extra == 'dataframe' - - distributed==2025.5.1 ; extra == 'distributed' + - distributed==2025.7.0 ; extra == 'distributed' - bokeh>=3.1.0 ; extra == 'diagnostics' - jinja2>=2.10.3 ; extra == 'diagnostics' - dask[array,dataframe,diagnostics,distributed] ; extra == 'complete' @@ -1172,35 +1180,36 @@ packages: version: 5.2.1 sha256: d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl - name: deprecated - version: 1.2.18 - sha256: bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec - requires_dist: - - wrapt>=1.10,<2 - - tox ; extra == 'dev' - - pytest ; extra == 'dev' - - pytest-cov ; extra == 'dev' - - bump2version<1 ; extra == 'dev' - - setuptools ; python_full_version >= '3.12' and extra == 'dev' - requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*' -- pypi: https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl name: distlib - version: 0.3.9 - sha256: 47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87 + version: 0.4.0 + sha256: 9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 - pypi: https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl name: docutils version: 0.21.2 sha256: dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2 requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl + name: donfig + version: 0.8.1.post1 + sha256: 2a3175ce74a06109ff9307d90a230f81215cbac9a751f4d1c6194644b8204f9d + requires_dist: + - pyyaml + - sphinx>=4.0.0 ; extra == 'docs' + - numpydoc ; extra == 'docs' + - pytest ; extra == 'docs' + - cloudpickle ; extra == 'docs' + - pytest ; extra == 'test' + - cloudpickle ; extra == 'test' + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/11/79/479e2194c9096b92aecdf33634ae948d2be306c6011673e98ee1917f32c2/dpkt-1.9.8-py3-none-any.whl name: dpkt version: 1.9.8 sha256: 4da4d111d7bf67575b571f5c678c71bddd2d8a01a3d57d489faf0a92c748fbfd -- pypi: https://files.pythonhosted.org/packages/90/a6/c8577b741974f106e24f8eb3efedc399be1a23cbbdcf49dd4bea5bb8aa4e/duckdb-1.3.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/68/fc/259a54fc22111a847981927aa58528d766e8b228c6d41deb0ad8a1959f9f/duckdb-1.3.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl name: duckdb - version: 1.3.1 - sha256: 8793b5abb365bbbf64ba3065f3a37951fe04f2d4506b0e24f3f8ecd08b3af4ba + version: 1.3.2 + sha256: 4732fb8cc60566b60e7e53b8c19972cb5ed12d285147a3063b16cc64a79f6d9f requires_python: '>=3.7.0' - pypi: https://files.pythonhosted.org/packages/cb/a3/460c57f094a4a165c84a1341c373b0a4f5ec6ac244b998d5021aade89b77/ecdsa-0.19.1-py2.py3-none-any.whl name: ecdsa @@ -1216,10 +1225,10 @@ packages: version: '0.4' sha256: f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f requires_python: '>=3.6' -- pypi: https://files.pythonhosted.org/packages/4a/18/3853de30cc92c164eccff6e62599ef88d0aa3b64740973b63e13f88145de/epicscorelibs-7.0.7.99.1.1.tar.gz +- pypi: https://files.pythonhosted.org/packages/a2/af/6bb0e1de4fb7218fa4ba95fe91ba1fd4a5661dd28a461eff6a0f609c996c/epicscorelibs-7.0.7.99.1.2-cp313-cp313-manylinux2014_x86_64.whl name: epicscorelibs - version: 7.0.7.99.1.1 - sha256: f9dd2c01913cf13959e882c7c2afccd5d55cd5084bd4597201ea119f65752231 + version: 7.0.7.99.1.2 + sha256: be8f328fe16b13fa9ca1dabf85c53561a3209b3c5df693c6b959351f6d11fabc requires_dist: - setuptools - setuptools-dso>=2.11a2 @@ -1230,10 +1239,10 @@ packages: version: 2.0.0 sha256: 7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/b1/f2/47e3f0e06c3bc3834db2a4ed01641e1ce493108461b9540dd5d2fb60d334/event_model-1.22.3-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/fb/32/e31e3363bf48ad2ba80b644b01ad9676ce154f1b755950de81eb4ed5b6bd/event_model-1.23.1-py3-none-any.whl name: event-model - version: 1.22.3 - sha256: 265f6730d35dc181bc6880e4d3ee94a888c39b1361a3f7bb12f4090acc538dad + version: 1.23.1 + sha256: e0b951b829cebcf3879beff238bb370fd997d315856bc5d5ac2a66202b854958 requires_dist: - importlib-resources - jsonschema>=4 @@ -1256,12 +1265,13 @@ packages: - ipython ; extra == 'dev' - matplotlib ; extra == 'dev' - numpydoc ; extra == 'dev' - - pydantic>=2.6 ; extra == 'dev' + - pydantic<2.11 ; extra == 'dev' + - datamodel-code-generator ; extra == 'dev' requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl name: executing - version: 2.2.0 - sha256: 11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa + version: 2.2.1 + sha256: 760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017 requires_dist: - asttokens>=2.1.0 ; extra == 'tests' - ipython ; extra == 'tests' @@ -1271,21 +1281,27 @@ packages: - littleutils ; extra == 'tests' - rich ; python_full_version >= '3.11' and extra == 'tests' requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/53/50/b1222562c6d270fea83e9c9075b8e8600b8479150a18e4516a6138b980d1/fastapi-0.115.14-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/e5/47/d63c60f59a59467fda0f93f46335c9d18526d7071f025cb5b89d5353ea42/fastapi-0.116.1-py3-none-any.whl name: fastapi - version: 0.115.14 - sha256: 6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca + version: 0.116.1 + sha256: c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565 requires_dist: - - starlette>=0.40.0,<0.47.0 + - starlette>=0.40.0,<0.48.0 - pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0 - typing-extensions>=4.8.0 - - fastapi-cli[standard]>=0.0.5 ; extra == 'standard' + - fastapi-cli[standard]>=0.0.8 ; extra == 'standard' - httpx>=0.23.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' - - fastapi-cli[standard]>=0.0.5 ; extra == 'all' + - fastapi-cli[standard-no-fastapi-cloud-cli]>=0.0.8 ; extra == 'standard-no-fastapi-cloud-cli' + - httpx>=0.23.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' + - fastapi-cli[standard]>=0.0.8 ; extra == 'all' - httpx>=0.23.0 ; extra == 'all' - jinja2>=3.1.5 ; extra == 'all' - python-multipart>=0.0.18 ; extra == 'all' @@ -1298,29 +1314,10 @@ packages: - pydantic-settings>=2.0.0 ; extra == 'all' - pydantic-extra-types>=2.0.0 ; extra == 'all' requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/61/bf/fd60001b3abc5222d8eaa4a204cd8c0ae78e75adc688f33ce4bf25b7fafa/fasteners-0.19-py3-none-any.whl - name: fasteners - version: '0.19' - sha256: 758819cb5d94cdedf4e836988b74de396ceacb8e2794d21f82d131fd9ee77237 - requires_python: '>=3.6' -- pypi: https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl name: filelock - version: 3.18.0 - sha256: c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de - requires_dist: - - furo>=2024.8.6 ; extra == 'docs' - - sphinx-autodoc-typehints>=3 ; extra == 'docs' - - sphinx>=8.1.3 ; extra == 'docs' - - covdefaults>=2.3 ; extra == 'testing' - - coverage>=7.6.10 ; extra == 'testing' - - diff-cover>=9.2.1 ; extra == 'testing' - - pytest-asyncio>=0.25.2 ; extra == 'testing' - - pytest-cov>=6 ; extra == 'testing' - - pytest-mock>=3.14 ; extra == 'testing' - - pytest-timeout>=2.3.1 ; extra == 'testing' - - pytest>=8.3.4 ; extra == 'testing' - - virtualenv>=20.28.1 ; extra == 'testing' - - typing-extensions>=4.12.2 ; python_full_version < '3.11' and extra == 'typing' + version: 3.19.1 + sha256: d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/27/cd/c883e1a7c447479d6e13985565080e3fea88ab5a107c21684c813dba1875/flexcache-0.3-py3-none-any.whl name: flexcache @@ -1344,10 +1341,10 @@ packages: - pytest-cov ; extra == 'test' - pytest-subtests ; extra == 'test' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/bb/61/78c7b3851add1481b048b5fdc29067397a1784e2910592bc81bb3f608635/fsspec-2025.5.1-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/2f/e0/014d5d9d7a4564cf1c40b5039bc882db69fd881111e03ab3657ac0b218e2/fsspec-2025.7.0-py3-none-any.whl name: fsspec - version: 2025.5.1 - sha256: 24d3a2e663d5fc735ab256263c4075f374a174c3410c0b25e5bd1970bceaa462 + version: 2025.7.0 + sha256: 8b012e39f63c7d5f10474de957f3ab793b47b45ae7d39f2fb735f8bbe25c0e21 requires_dist: - adlfs ; extra == 'abfs' - adlfs ; extra == 'adl' @@ -1355,7 +1352,7 @@ packages: - dask ; extra == 'dask' - distributed ; extra == 'dask' - pre-commit ; extra == 'dev' - - ruff ; extra == 'dev' + - ruff>=0.5 ; extra == 'dev' - numpydoc ; extra == 'doc' - sphinx ; extra == 'doc' - sphinx-design ; extra == 'doc' @@ -1448,48 +1445,50 @@ packages: - tqdm ; extra == 'test-full' - urllib3 ; extra == 'test-full' - zarr ; extra == 'test-full' - - zstandard ; extra == 'test-full' + - zstandard ; python_full_version < '3.14' and extra == 'test-full' - tqdm ; extra == 'tqdm' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/27/48/e791a7ed487dbb9729ef32bb5d1af16693d8925f4366befef54119b2e576/furo-2024.8.6-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/3a/34/2b07b72bee02a63241d654f5d8af87a2de977c59638eec41ca356ab915cd/furo-2025.7.19-py3-none-any.whl name: furo - version: 2024.8.6 - sha256: 6cd97c58b47813d3619e63e9081169880fbe331f0ca883c871ff1f3f11814f5c + version: 2025.7.19 + sha256: bdea869822dfd2b494ea84c0973937e35d1575af088b6721a29c7f7878adc9e3 requires_dist: - beautifulsoup4 - sphinx>=6.0,<9.0 - sphinx-basic-ng>=1.0.0b2 - pygments>=2.7 + - accessible-pygments>=0.0.5 requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/26/f2/ad51331a157c7015c675702e2d5230c243695c788f8f75feba1af32b3617/greenlet-3.2.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl name: greenlet - version: 3.2.3 - sha256: 2c724620a101f8170065d7dded3f962a2aea7a7dae133a009cada42847e04a7b + version: 3.2.4 + sha256: 23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671 requires_dist: - sphinx ; extra == 'docs' - furo ; extra == 'docs' - objgraph ; extra == 'test' - psutil ; extra == 'test' + - setuptools ; extra == 'test' requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl name: h11 version: 0.16.0 sha256: 63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/4e/27/ca28b77f4cf613282e4ea9bac360cdfa8db8b4d2154e2f0bd82fd26baeb3/h5netcdf-1.6.3-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/5b/f5/ac71e692aad076d50a0f5f073204346d5f5577daffd21bb4b72c485f8959/h5netcdf-1.6.4-py3-none-any.whl name: h5netcdf - version: 1.6.3 - sha256: b79bc24d1f8b1cdc1f16b213753209411e08af04e35b0bcf5ce5cf7fb4023572 + version: 1.6.4 + sha256: e0018e6a918f2bef2a4aa7b470a952b8a0b5d16a5893d59bea56524cc6207fcf requires_dist: - h5py - packaging - netcdf4 ; extra == 'test' - pytest ; extra == 'test' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/fd/41/bc2df86b72965775f6d621e0ee269a5f3ac23e8f870abf519de9c7d93b4d/h5py-3.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/e7/ec/86f59025306dcc6deee5fda54d980d077075b8d9889aac80f158bd585f1b/h5py-3.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl name: h5py - version: 3.13.0 - sha256: be949b46b7388074c5acae017fbbe3e5ba303fd9daaa52157fdfef30bbdacadd + version: 3.14.0 + sha256: d90e6445ab7c146d7f7981b11895d70bc1dd91278a4f9f9028bc0c95e4a53f13 requires_dist: - numpy>=1.19.3 requires_python: '>=3.9' @@ -1547,10 +1546,10 @@ packages: - socksio==1.* ; extra == 'socks' - zstandard>=0.18.0 ; extra == 'zstd' requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/2b/d3/85feeba1d097b81a44bcffa6a0beab7b4dfffe78e82fc54978d3ac380736/identify-2.6.10-py2.py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/e7/ce/461b60a3ee109518c055953729bf9ed089a04db895d47e95444071dcdef2/identify-2.6.13-py2.py3-none-any.whl name: identify - version: 2.6.10 - sha256: 5f34248f54136beed1a7ba6a6b5c4b6cf21ff495aac7c359e1ef831ae3b8ab25 + version: 2.6.13 + sha256: 60381139b3ae39447482ecc406944190f690d4a2997f2584062089848361b33b requires_dist: - ukkonen ; extra == 'license' requires_python: '>=3.9' @@ -1569,10 +1568,10 @@ packages: version: 1.4.1 sha256: 0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*' -- pypi: https://files.pythonhosted.org/packages/79/9d/0fb148dc4d6fa4a7dd1d8378168d9b4cd8d4560a6fbf6f0121c5fc34eb68/importlib_metadata-8.6.1-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl name: importlib-metadata - version: 8.6.1 - sha256: 02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e + version: 8.7.0 + sha256: e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd requires_dist: - zipp>=3.20 - typing-extensions>=3.6.4 ; python_full_version < '3.8' @@ -1622,10 +1621,10 @@ packages: version: 2.1.0 sha256: 9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760 requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/b2/9d/4ff2adf55d1b6e3777b0303fdbe5b723f76e46cba4a53a32fe82260d2077/ipython-9.1.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/08/2a/5628a99d04acb2d2f2e749cdf4ea571d2575e898df0528a090948018b726/ipython-9.5.0-py3-none-any.whl name: ipython - version: 9.1.0 - sha256: 2df07257ec2f84a6b346b8d83100bcf8fa501c6e01ab75cd3799b0bb253b3d2a + version: 9.5.0 + sha256: 88369ffa1d5817d609120daa523a6da06d02518e582347c29f8451732a9c5e72 requires_dist: - colorama ; sys_platform == 'win32' - decorator @@ -1651,7 +1650,7 @@ packages: - sphinx>=1.3 ; extra == 'doc' - typing-extensions ; extra == 'doc' - pytest ; extra == 'test' - - pytest-asyncio<0.22 ; extra == 'test' + - pytest-asyncio ; extra == 'test' - testpath ; extra == 'test' - packaging ; extra == 'test' - ipython[test] ; extra == 'test-extra' @@ -1744,15 +1743,13 @@ packages: version: 3.0.0 sha256: 13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942 requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl name: jsonschema - version: 4.24.0 - sha256: a462455f19f5faf404a7902952b6f0e3ce868f3ee09a359b05eca6673bd8412d + version: 4.25.1 + sha256: 3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63 requires_dist: - attrs>=22.2.0 - - importlib-resources>=1.4.0 ; python_full_version < '3.9' - jsonschema-specifications>=2023.3.6 - - pkgutil-resolve-name>=1.3.10 ; python_full_version < '3.9' - referencing>=0.28.4 - rpds-py>=0.7.1 - fqdn ; extra == 'format' @@ -1769,6 +1766,7 @@ packages: - jsonpointer>1.13 ; extra == 'format-nongpl' - rfc3339-validator ; extra == 'format-nongpl' - rfc3986-validator>0.1.0 ; extra == 'format-nongpl' + - rfc3987-syntax>=1.1.0 ; extra == 'format-nongpl' - uri-template ; extra == 'format-nongpl' - webcolors>=24.6.0 ; extra == 'format-nongpl' requires_python: '>=3.9' @@ -1779,31 +1777,31 @@ packages: requires_dist: - referencing>=0.31.0 requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_4.conda - sha256: db73f38155d901a610b2320525b9dd3b31e4949215c870685fd92ea61b5ce472 - md5: 01f8d123c96816249efd255a31ad7712 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda + sha256: 1a620f27d79217c1295049ba214c2f80372062fd251b569e9873d4a953d27554 + md5: 0be7c6e070c19105f966d3758448d018 depends: - __glibc >=2.17,<3.0.a0 constrains: - - binutils_impl_linux-64 2.43 + - binutils_impl_linux-64 2.44 license: GPL-3.0-only license_family: GPL purls: [] - size: 671240 - timestamp: 1740155456116 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda - sha256: 33ab03438aee65d6aa667cf7d90c91e5e7d734c19a67aa4c7040742c0a13d505 - md5: db0bfbe7dd197b68ad5f30333bae6ce0 + size: 676044 + timestamp: 1752032747103 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda + sha256: da2080da8f0288b95dd86765c801c6e166c4619b910b11f9a8446fb852438dc2 + md5: 4211416ecba1866fab0c6470986c22d6 depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 + - libgcc >=14 constrains: - - expat 2.7.0.* + - expat 2.7.1.* license: MIT license_family: MIT purls: [] - size: 74427 - timestamp: 1743431794976 + size: 74811 + timestamp: 1752719572741 - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda sha256: 764432d32db45466e87f10621db5b74363a9f847d2b8b1f9743746cd160f06ab md5: ede4673863426c0883c0063d853bbd85 @@ -1815,72 +1813,74 @@ packages: purls: [] size: 57433 timestamp: 1743434498161 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.2.0-h767d61c_2.conda - sha256: 3a572d031cb86deb541d15c1875aaa097baefc0c580b54dc61f5edab99215792 - md5: ef504d1acbd74b7cc6849ef8af47dd03 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_4.conda + sha256: 144e35c1c2840f2dc202f6915fc41879c19eddbb8fa524e3ca4aa0d14018b26f + md5: f406dcbb2e7bef90d793e50e79a2882b depends: - __glibc >=2.17,<3.0.a0 - _openmp_mutex >=4.5 constrains: - - libgomp 14.2.0 h767d61c_2 - - libgcc-ng ==14.2.0=*_2 + - libgcc-ng ==15.1.0=*_4 + - libgomp 15.1.0 h767d61c_4 license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] - size: 847885 - timestamp: 1740240653082 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.2.0-h69a702a_2.conda - sha256: fb7558c328b38b2f9d2e412c48da7890e7721ba018d733ebdfea57280df01904 - md5: a2222a6ada71fb478682efe483ce0f92 + size: 824153 + timestamp: 1753903866511 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_4.conda + sha256: 76ceac93ed98f208363d6e9c75011b0ff7b97b20f003f06461a619557e726637 + md5: 28771437ffcd9f3417c66012dc49a3be depends: - - libgcc 14.2.0 h767d61c_2 + - libgcc 15.1.0 h767d61c_4 license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] - size: 53758 - timestamp: 1740240660904 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.2.0-h767d61c_2.conda - sha256: 1a3130e0b9267e781b89399580f3163632d59fe5b0142900d63052ab1a53490e - md5: 06d02030237f4d5b3d9a7e7d348fe3c6 + size: 29249 + timestamp: 1753903872571 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_4.conda + sha256: e0487a8fec78802ac04da0ac1139c3510992bc58a58cde66619dde3b363c2933 + md5: 3baf8976c96134738bba224e9ef6b1e5 depends: - __glibc >=2.17,<3.0.a0 license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] - size: 459862 - timestamp: 1740240588123 -- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_0.conda - sha256: f4f21dfc54b08d462f707b771ecce3fa9bc702a2a05b55654f64154f48b141ef - md5: 0e87378639676987af32fee53ba32258 + size: 447289 + timestamp: 1753903801049 +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda + sha256: f2591c0069447bbe28d4d696b7fcb0c5bd0b4ac582769b89addbcf26fb3430d8 + md5: 1a580f7796c7bf6393fddb8bbbde58dc depends: - __glibc >=2.17,<3.0.a0 - libgcc >=13 + constrains: + - xz 5.8.1.* license: 0BSD purls: [] - size: 112709 - timestamp: 1743771086123 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-h4bc722e_0.conda - sha256: d02d1d3304ecaf5c728e515eb7416517a0b118200cd5eacbe829c432d1664070 - md5: aeb98fdeb2e8f25d43ef71fbacbeec80 + size: 112894 + timestamp: 1749230047870 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda + sha256: 3aa92d4074d4063f2a162cd8ecb45dccac93e543e565c01a787e16a43501f7ee + md5: c7e925f37e3b40d893459e625f6a53f1 depends: - __glibc >=2.17,<3.0.a0 - - libgcc-ng >=12 + - libgcc >=13 license: BSD-2-Clause license_family: BSD purls: [] - size: 89991 - timestamp: 1723817448345 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.49.1-hee588c1_2.conda - sha256: a086289bf75c33adc1daed3f1422024504ffb5c3c8b3285c49f025c29708ed16 - md5: 962d6ac93c30b1dfc54c9cccafd1003e + size: 91183 + timestamp: 1748393666725 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda + sha256: 6d9c32fc369af5a84875725f7ddfbfc2ace795c28f246dc70055a79f9b2003da + md5: 0b367fad34931cb79e0d6b7e5c06bb1c depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 + - libgcc >=14 - libzlib >=1.3.1,<2.0a0 - license: Unlicense + license: blessing purls: [] - size: 918664 - timestamp: 1742083674731 + size: 932581 + timestamp: 1753948484112 - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda sha256: 787eb542f055a2b3de553614b25f09eefb0a0931b0c87dbcce6efdfd92f04f18 md5: 40b61aab5c7ba9ff276c41cfffe6b80b @@ -1979,12 +1979,12 @@ packages: requires_dist: - traitlets requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/a7/f7/7782a043553ee469c1ff49cfa1cdace2d6bf99a1f333cf38676b3ddf30da/mdit_py_plugins-0.4.2-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl name: mdit-py-plugins - version: 0.4.2 - sha256: 0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636 + version: 0.5.0 + sha256: 07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f requires_dist: - - markdown-it-py>=1.0.0,<4.0.0 + - markdown-it-py>=2.0.0,<5.0.0 - pre-commit ; extra == 'code-style' - myst-parser ; extra == 'rtd' - sphinx-book-theme ; extra == 'rtd' @@ -1992,16 +1992,16 @@ packages: - pytest ; extra == 'testing' - pytest-cov ; extra == 'testing' - pytest-regressions ; extra == 'testing' - requires_python: '>=3.8' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl name: mdurl version: 0.1.2 sha256: 84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/2d/7b/2c1d74ca6c94f70a1add74a8393a0138172207dc5de6fc6269483519d048/msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/20/22/2ebae7ae43cd8f2debc35c631172ddf14e2a87ffcc04cf43ff9df9fff0d3/msgpack-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl name: msgpack - version: 1.1.0 - sha256: 0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca + version: 1.1.1 + sha256: 9d592d06e3cc2f537ceeeb23d38799c6ad83255289bb84c2e5792e5a8dea268a 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 @@ -2068,28 +2068,26 @@ packages: name: netifaces version: 0.11.0 sha256: 043a79146eb2907edf439899f262b3dfe41717d34124298ed281139a8b93ca32 -- pypi: https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl name: networkx - version: 3.4.2 - sha256: df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f + version: '3.5' + sha256: 0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec requires_dist: - - numpy>=1.24 ; extra == 'default' - - scipy>=1.10,!=1.11.0,!=1.11.1 ; extra == 'default' - - matplotlib>=3.7 ; extra == 'default' + - numpy>=1.25 ; extra == 'default' + - scipy>=1.11.2 ; extra == 'default' + - matplotlib>=3.8 ; extra == 'default' - pandas>=2.0 ; extra == 'default' - - changelist==0.5 ; extra == 'developer' - - pre-commit>=3.2 ; extra == 'developer' - - mypy>=1.1 ; extra == 'developer' - - rtoml ; extra == 'developer' - - sphinx>=7.3 ; extra == 'doc' - - pydata-sphinx-theme>=0.15 ; extra == 'doc' - - sphinx-gallery>=0.16 ; extra == 'doc' + - pre-commit>=4.1 ; extra == 'developer' + - mypy>=1.15 ; extra == 'developer' + - sphinx>=8.0 ; extra == 'doc' + - pydata-sphinx-theme>=0.16 ; extra == 'doc' + - sphinx-gallery>=0.18 ; extra == 'doc' - numpydoc>=1.8.0 ; extra == 'doc' - - pillow>=9.4 ; extra == 'doc' + - pillow>=10 ; extra == 'doc' - texext>=0.6.7 ; extra == 'doc' - myst-nb>=1.1 ; extra == 'doc' - intersphinx-registry ; extra == 'doc' - - osmnx>=1.9 ; extra == 'example' + - osmnx>=2.0.0 ; extra == 'example' - momepy>=0.7.2 ; extra == 'example' - contextily>=1.6 ; extra == 'example' - seaborn>=0.13 ; extra == 'example' @@ -2102,7 +2100,10 @@ packages: - sympy>=1.10 ; extra == 'extra' - pytest>=7.2 ; extra == 'test' - pytest-cov>=4.0 ; extra == 'test' - requires_python: '>=3.10' + - pytest-xdist>=3.0 ; extra == 'test' + - pytest-mpl ; extra == 'test-extras' + - pytest-randomly ; extra == 'test-extras' + requires_python: '>=3.11' - pypi: https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl name: nodeenv version: 1.9.1 @@ -2116,13 +2117,13 @@ packages: - llvmlite>=0.44.0.dev0,<0.45 - numpy>=1.24,<2.3 requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/fa/91/d96999b41e3146b6c0ce6bddc5ad85803cb4d743c95394562c2a4bb8cded/numcodecs-0.15.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/55/7f/0ab8db32ef9b51c60f7b759c2b155e1edcb08febb508c22a9d04b19ec735/numcodecs-0.16.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl name: numcodecs - version: 0.15.1 - sha256: 1dfdea4a67108205edfce99c1cb6cd621343bc7abb7e16a041c966776920e7de + version: 0.16.2 + sha256: 8a18ced1ecbdd0e9ee716820dbb3a094c896eed8005273bbcab9980bdac270ae requires_dist: - numpy>=1.24 - - deprecated + - typing-extensions - sphinx ; extra == 'docs' - sphinx-issues ; extra == 'docs' - pydata-sphinx-theme ; extra == 'docs' @@ -2130,6 +2131,7 @@ packages: - coverage ; extra == 'test' - pytest ; extra == 'test' - pytest-cov ; extra == 'test' + - pyzstd ; extra == 'test' - importlib-metadata ; extra == 'test-extras' - msgpack ; extra == 'msgpack' - zfpy>=1.0.0 ; extra == 'zfpy' @@ -2143,10 +2145,10 @@ packages: requires_dist: - numpy>=1.23.0 requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/aa/fc/ebfd32c3e124e6a1043e19c0ab0769818aa69050ce5589b63d05ff185526/numpy-2.2.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl name: numpy - version: 2.2.5 - sha256: 2ba321813a00e508d5421104464510cc962a6f791aa2fca1c97b1e65027da80d + version: 2.2.6 + sha256: 1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl name: openpyxl @@ -2155,30 +2157,30 @@ packages: requires_dist: - et-xmlfile requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.0-h7b32b05_0.conda - sha256: 38285d280f84f1755b7c54baf17eccf2e3e696287954ce0adca16546b85ee62c - md5: bb539841f2a3fde210f387d00ed4bb9d +- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda + sha256: c9f54d4e8212f313be7b02eb962d0cb13a8dae015683a403d3accd4add3e520e + md5: ffffb341206dd0dab0c36053c048d621 depends: - __glibc >=2.17,<3.0.a0 - ca-certificates - - libgcc >=13 + - libgcc >=14 license: Apache-2.0 license_family: Apache purls: [] - size: 3121673 - timestamp: 1744132167438 -- pypi: https://files.pythonhosted.org/packages/12/f2/89ea3361a305466bc6460a532188830351220b5f0851a5fa133155c16eca/opentelemetry_api-1.32.1-py3-none-any.whl + size: 3128847 + timestamp: 1754465526100 +- pypi: https://files.pythonhosted.org/packages/bb/ee/6b08dde0a022c463b88f55ae81149584b125a42183407dc1045c486cc870/opentelemetry_api-1.36.0-py3-none-any.whl name: opentelemetry-api - version: 1.32.1 - sha256: bbd19f14ab9f15f0e85e43e6a958aa4cb1f36870ee62b7fd205783a112012724 + version: 1.36.0 + sha256: 02f20bcacf666e1333b6b1f04e647dc1d5111f86b8e510238fcc56d7762cda8c requires_dist: - - deprecated>=1.2.6 - - importlib-metadata>=6.0,<8.7.0 - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/f2/5e/f5ded172af40b7e7ae60697a2b2096cec53e45344edce41e91a74425473c/ophyd-1.10.5-py3-none-any.whl + - importlib-metadata>=6.0,<8.8.0 + - typing-extensions>=4.5.0 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/fa/b3/1e63820f1a4df7854e33f0c3c5fa6be758c7fb7ea007ae9c8b9b35f48690/ophyd-1.10.7-py3-none-any.whl name: ophyd - version: 1.10.5 - sha256: 9acdc0b69b9f51bc8ee50cfaff11c18a5e089dd66f13ce8fe2ea57e291bf959a + version: 1.10.7 + sha256: d34a385a603d6ee255507dd2992ccb72a4316b824bf226bd2c8d2f0ce825bfbe requires_dist: - networkx>=2.0 - numpy @@ -2188,7 +2190,7 @@ packages: - attrs>=19.3.0 ; extra == 'dev' - black==22.3.0 ; extra == 'dev' - bluesky>=1.11.0 ; extra == 'dev' - - caproto[standard]>=0.4.2rc1 ; extra == 'dev' + - caproto[standard]>=0.4.2rc1,!=1.2.0 ; extra == 'dev' - pytest-codecov ; extra == 'dev' - databroker>=1.0.0b1 ; extra == 'dev' - doctr ; extra == 'dev' @@ -2219,20 +2221,21 @@ packages: - sphinx-design ; extra == 'dev' - tox-direct ; extra == 'dev' requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/85/38/6b455b1d6f0b9aa3db4e8888b797f77d856a8350d836c66a43d1078573b2/ophyd_async-0.10.0a4-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/c8/cb/75a17d45ae070fa4af0b6bf79fb820b9d9137bf09f485a6728f71e4a062e/ophyd_async-0.12.3-py3-none-any.whl name: ophyd-async - version: 0.10.0a4 - sha256: 00992baf393b95f707b332aa74d2b1b82453c4bdf015b52d5f419920d768f878 + version: 0.12.3 + sha256: 3d0e98f9ab0169776dad24c072ef19ece6d53faa2933e55d3c139c525761b802 requires_dist: - numpy - bluesky>=1.13.1rc2 - - event-model>=1.22.1 + - event-model>=1.23 - pyyaml - colorlog - pydantic>=2.0 - pydantic-numpy + - stamina>=23.1.0 - h5py ; extra == 'sim' - - aioca>=1.6 ; extra == 'ca' + - aioca>=2.0a4 ; extra == 'ca' - p4p>=4.2.0 ; extra == 'pva' - pytango==10.0.0 ; extra == 'tango' - ipython ; extra == 'demo' @@ -2247,7 +2250,7 @@ packages: - import-linter ; extra == 'dev' - myst-parser ; extra == 'dev' - numpydoc ; extra == 'dev' - - ophyd ; extra == 'dev' + - ophyd>=1.10.7 ; extra == 'dev' - pickleshare ; extra == 'dev' - pipdeptree ; extra == 'dev' - pre-commit ; extra == 'dev' @@ -2271,20 +2274,20 @@ packages: - types-mock ; extra == 'dev' - types-pyyaml ; extra == 'dev' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/6d/4c/2bda09855c6b5f2c055034c9eda1529967b042ff8d81a05005115c4e6772/orjson-3.10.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/d0/b4/f98355eff0bd1a38454209bbc73372ce351ba29933cb3e2eba16c04b9448/orjson-3.11.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl name: orjson - version: 3.10.18 - sha256: bb70d489bc79b7519e5803e2cc4c72343c9dc1154258adf2f8925d0b60da7c58 + version: 3.11.3 + sha256: b822caf5b9752bc6f246eb08124c3d12bf2175b66ab74bac2ef3bbf9221ce1b2 requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl name: packaging version: '25.0' sha256: 29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/8f/52/0634adaace9be2d8cac9ef78f05c47f3a675882e068438b9d7ec7ef0c13f/pandas-2.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl name: pandas - version: 2.2.3 - sha256: f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24 + version: 2.3.2 + sha256: 4ac8c320bded4718b298281339c1a50fb00a6ba78cb2a63521c39bec95b0209b requires_dist: - numpy>=1.22.4 ; python_full_version < '3.11' - numpy>=1.23.2 ; python_full_version == '3.11.*' @@ -2372,16 +2375,16 @@ packages: - xlsxwriter>=3.0.5 ; extra == 'all' - zstandard>=0.19.0 ; extra == 'all' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl name: parso - version: 0.8.4 - sha256: a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18 + version: 0.8.5 + sha256: 646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887 requires_dist: + - pytest ; extra == 'testing' + - docopt ; extra == 'testing' - flake8==5.0.4 ; extra == 'qa' - mypy==0.971 ; extra == 'qa' - types-setuptools==67.2.0.1 ; extra == 'qa' - - docopt ; extra == 'testing' - - pytest ; extra == 'testing' requires_python: '>=3.6' - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl name: partd @@ -2431,38 +2434,71 @@ packages: - typing-extensions ; python_full_version < '3.10' and extra == 'typing' - defusedxml ; extra == 'xmp' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/b7/16/bd2f5904557265882108dc2e04f18abc05ab0c2b7082ae9430091daf1d5c/Pint-0.24.4-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/76/cc/c528311d798e22ec884b816e8aa2989e0f1f28cdc8e5969e2be5f10bce85/pint-0.25-py3-none-any.whl name: pint - version: 0.24.4 - sha256: aa54926c8772159fcf65f82cc0d34de6768c151b32ad1deb0331291c38fe7659 + version: '0.25' + sha256: cc20ae3dff010b9bbea41fb80c2de008f683cc83512cea73633d55aead80aa1e requires_dist: - - platformdirs>=2.1.0 - - typing-extensions>=4.0.0 - flexcache>=0.3 - flexparser>=0.4 + - platformdirs>=2.1.0 + - typing-extensions>=4.0.0 + - babel<=2.8 ; extra == 'all' + - dask<2025.3.0 ; extra == 'all' + - matplotlib ; extra == 'all' + - mip>=1.13 ; python_full_version < '3.13' and extra == 'all' + - numpy>=1.23 ; extra == 'all' + - pint-pandas>=0.3 ; extra == 'all' + - uncertainties>=3.1.6 ; extra == 'all' + - xarray ; extra == 'all' - babel<=2.8 ; extra == 'babel' - - pytest ; extra == 'bench' - - pytest-codspeed ; extra == 'bench' - - dask ; extra == 'dask' - - mip>=1.13 ; extra == 'mip' + - pytest ; extra == 'codspeed' + - pytest-benchmark ; extra == 'codspeed' + - pytest-codspeed ; extra == 'codspeed' + - pytest-cov ; extra == 'codspeed' + - pytest-mpl ; extra == 'codspeed' + - pytest-subtests ; extra == 'codspeed' + - dask<2025.3.0 ; extra == 'dask' + - babel ; extra == 'docs' + - commonmark==0.8.1 ; extra == 'docs' + - docutils ; extra == 'docs' + - graphviz ; extra == 'docs' + - ipykernel ; extra == 'docs' + - ipython<=8.12 ; extra == 'docs' + - jupyter-client ; extra == 'docs' + - nbsphinx ; extra == 'docs' + - pooch ; extra == 'docs' + - pygments>=2.4 ; extra == 'docs' + - recommonmark==0.5.0 ; extra == 'docs' + - sciform ; extra == 'docs' + - scipy ; extra == 'docs' + - serialize ; extra == 'docs' + - sparse ; extra == 'docs' + - sphinx-book-theme>=1.1.0 ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-design ; extra == 'docs' + - sphinx>=6,<8.2 ; extra == 'docs' + - matplotlib ; extra == 'matplotlib' + - mip>=1.13 ; python_full_version < '3.13' and extra == 'mip' - numpy>=1.23 ; extra == 'numpy' - pint-pandas>=0.3 ; extra == 'pandas' - pytest ; extra == 'test' - - pytest-mpl ; extra == 'test' + - pytest-benchmark ; extra == 'test' - pytest-cov ; extra == 'test' - pytest-subtests ; extra == 'test' - - pytest-benchmark ; extra == 'test' - - pytest ; extra == 'testbase' - - pytest-cov ; extra == 'testbase' - - pytest-subtests ; extra == 'testbase' - - pytest-benchmark ; extra == 'testbase' + - pytest ; extra == 'test-all' + - pytest-benchmark ; extra == 'test-all' + - pytest-cov ; extra == 'test-all' + - pytest-mpl ; extra == 'test-all' + - pytest-subtests ; extra == 'test-all' + - pytest-mpl ; extra == 'test-mpl' - uncertainties>=3.1.6 ; extra == 'uncertainties' - xarray ; extra == 'xarray' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl name: platformdirs - version: 4.3.7 - sha256: a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94 + version: 4.4.0 + sha256: abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85 requires_dist: - furo>=2024.8.6 ; extra == 'docs' - proselint>=0.14 ; extra == 'docs' @@ -2475,20 +2511,21 @@ packages: - pytest>=8.3.4 ; extra == 'test' - mypy>=1.14.1 ; extra == 'type' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl name: pluggy - version: 1.5.0 - sha256: 44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 + version: 1.6.0 + sha256: e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 requires_dist: - pre-commit ; extra == 'dev' - tox ; extra == 'dev' - pytest ; extra == 'testing' - pytest-benchmark ; extra == 'testing' - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl + - coverage ; extra == 'testing' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl name: pre-commit - version: 4.2.0 - sha256: a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd + version: 4.3.0 + sha256: 2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8 requires_dist: - cfgv>=2.0.0 - identify>=1.0.0 @@ -2503,10 +2540,10 @@ packages: requires_dist: - twisted ; extra == 'twisted' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl name: prompt-toolkit - version: 3.0.51 - sha256: 52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07 + version: 3.0.52 + sha256: 9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955 requires_dist: - wcwidth requires_python: '>=3.8' @@ -2524,10 +2561,10 @@ packages: name: py-cpuinfo version: 9.0.0 sha256: 859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5 -- pypi: https://files.pythonhosted.org/packages/be/27/43a47fa0ff9053ab5203bb3faeec435d43c0d8bfa40179bfd076cdbd4e1c/pyarrow-20.0.0-cp313-cp313-manylinux_2_28_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/89/4b/7782438b551dbb0468892a276b8c789b8bbdb25ea5c5eb27faadd753e037/pyarrow-21.0.0-cp313-cp313-manylinux_2_28_x86_64.whl name: pyarrow - version: 20.0.0 - sha256: 97c8dc984ed09cb07d618d57d8d4b67a5100a30c3818c2fb0b04599f0da2de7b + version: 21.0.0 + sha256: 69cbbdf0631396e9925e048cfa5bce4e8c3d3b41562bbd70c685a8eb53a91e61 requires_dist: - pytest ; extra == 'test' - hypothesis ; extra == 'test' @@ -2545,10 +2582,10 @@ packages: version: '2.22' sha256: c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl name: pydantic - version: 2.11.5 - sha256: f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7 + version: 2.11.7 + sha256: dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b requires_dist: - annotated-types>=0.6.0 - pydantic-core==2.33.2 @@ -2591,17 +2628,17 @@ packages: - tomli>=2.0.1 ; extra == 'toml' - pyyaml>=6.0.1 ; extra == 'yaml' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl name: pygments - version: 2.19.1 - sha256: 9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c + version: 2.19.2 + sha256: 86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b requires_dist: - colorama>=0.4.6 ; extra == 'windows-terminal' requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/49/b6/b04e5c2f41a5ccad74a1a4759da41adb20b4bc9d59a5e08d29ba60084d07/pyright-1.1.403-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/84/30/89aa7f7d7a875bbb9a577d4b1dc5a3e404e3d2ae2657354808e905e358e0/pyright-1.1.404-py3-none-any.whl name: pyright - version: 1.1.403 - sha256: c0eeca5aa76cbef3fcc271259bbd785753c7ad7bcac99a9162b4c4c7daed23b3 + version: 1.1.404 + sha256: c7b7ff1fdb7219c643079e4c3e7d4125f0dafcc19d253b47e898d130ea426419 requires_dist: - nodeenv>=1.6.0 - typing-extensions>=4.1 @@ -2610,31 +2647,32 @@ packages: - twine>=3.4.1 ; extra == 'dev' - nodejs-wheel-binaries ; extra == 'nodejs' requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl name: pytest - version: 8.3.5 - sha256: c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820 + version: 8.4.1 + sha256: 539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7 requires_dist: - - colorama ; sys_platform == 'win32' - - exceptiongroup>=1.0.0rc8 ; python_full_version < '3.11' - - iniconfig - - packaging + - colorama>=0.4 ; sys_platform == 'win32' + - exceptiongroup>=1 ; python_full_version < '3.11' + - iniconfig>=1 + - packaging>=20 - pluggy>=1.5,<2 + - pygments>=2.7.2 - tomli>=1 ; python_full_version < '3.11' - argcomplete ; extra == 'dev' - attrs>=19.2 ; extra == 'dev' - hypothesis>=3.56 ; extra == 'dev' - mock ; extra == 'dev' - - pygments>=2.7.2 ; extra == 'dev' - requests ; extra == 'dev' - setuptools ; extra == 'dev' - xmlschema ; extra == 'dev' - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/30/05/ce271016e351fddc8399e546f6e23761967ee09c8c568bbfbecb0c150171/pytest_asyncio-1.0.0-py3-none-any.whl + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/c7/9d/bf86eddabf8c6c9cb1ea9a869d6873b46f105a5d292d3a6f7071f5b07935/pytest_asyncio-1.1.0-py3-none-any.whl name: pytest-asyncio - version: 1.0.0 - sha256: 4f024da9f1ef945e680dc68610b52550e36590a67fd31bb3b4943979a1f90ef3 + version: 1.1.0 + sha256: 5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf requires_dist: + - backports-asyncio-runner>=1.1,<2 ; python_full_version < '3.11' - pytest>=8.2,<9 - typing-extensions>=4.12 ; python_full_version < '3.10' - sphinx>=5.3 ; extra == 'docs' @@ -2642,23 +2680,24 @@ packages: - coverage>=6.2 ; extra == 'testing' - hypothesis>=5.7.1 ; extra == 'testing' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl name: pytest-cov - version: 6.1.1 - sha256: bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde + version: 6.2.1 + sha256: f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5 requires_dist: - - pytest>=4.6 + - pytest>=6.2.5 - coverage[toml]>=7.5 + - pluggy>=1.2 - fields ; extra == 'testing' - hunter ; extra == 'testing' - process-tests ; extra == 'testing' - pytest-xdist ; extra == 'testing' - virtualenv ; extra == 'testing' requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.3-hf636f53_101_cp313.conda - build_number: 101 - sha256: eecb11ea60f8143deeb301eab2e04d04f7acb83659bb20fdfeacd431a5f31168 - md5: 10622e12d649154af0bd76bcf33a7c5c +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.5-hec9711d_102_cp313.conda + build_number: 102 + sha256: c2cdcc98ea3cbf78240624e4077e164dc9d5588eefb044b4097c3df54d24d504 + md5: 89e07d92cf50743886f41638d58c4328 depends: - __glibc >=2.17,<3.0.a0 - bzip2 >=1.0.8,<2.0a0 @@ -2668,7 +2707,7 @@ packages: - libgcc >=13 - liblzma >=5.8.1,<6.0a0 - libmpdec >=4.0.0,<5.0a0 - - libsqlite >=3.49.1,<4.0a0 + - libsqlite >=3.50.1,<4.0a0 - libuuid >=2.38.1,<3.0a0 - libzlib >=1.3.1,<2.0a0 - ncurses >=6.5,<7.0a0 @@ -2679,8 +2718,8 @@ packages: - tzdata license: Python-2.0 purls: [] - size: 33268245 - timestamp: 1744665022734 + size: 33273132 + timestamp: 1750064035176 python_site_packages_path: lib/python3.13/site-packages - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl name: python-dateutil @@ -2715,17 +2754,17 @@ packages: version: 0.0.20 sha256: 8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104 requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-7_cp313.conda - build_number: 7 - sha256: 0595134584589064f56e67d3de1d8fcbb673a972946bce25fb593fb092fdcd97 - md5: e84b44e6300f1703cb25d29120c5b1d8 +- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + build_number: 8 + sha256: 210bffe7b121e651419cb196a2a63687b087497595c9be9d20ebe97dd06060a7 + md5: 94305520c52a4aa3f6c2b1ff6008d9f8 constrains: - python 3.13.* *_cp313 license: BSD-3-Clause license_family: BSD purls: [] - size: 6988 - timestamp: 1745258852285 + size: 7002 + timestamp: 1752805902938 - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl name: pytz version: '2025.2' @@ -2746,6 +2785,18 @@ packages: purls: [] size: 282480 timestamp: 1740379431762 +- pypi: https://files.pythonhosted.org/packages/e8/02/89e2ed7e85db6c93dfa9e8f691c5087df4e3551ab39081a4d7c6d1f90e05/redis-6.4.0-py3-none-any.whl + name: redis + version: 6.4.0 + sha256: f0544fa9604264e9464cdf4814e7d4830f74b165d52f2a330a760a88dd248b7f + requires_dist: + - async-timeout>=4.0.3 ; python_full_version < '3.11.3' + - hiredis>=3.2.0 ; extra == 'hiredis' + - pyjwt>=2.9.0 ; extra == 'jwt' + - cryptography>=36.0.1 ; extra == 'ocsp' + - pyopenssl>=20.0.1 ; extra == 'ocsp' + - requests>=2.31.0 ; extra == 'ocsp' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl name: referencing version: 0.36.2 @@ -2755,10 +2806,10 @@ packages: - rpds-py>=0.7.0 - typing-extensions>=4.4.0 ; python_full_version < '3.13' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl name: requests - version: 2.32.3 - sha256: 70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 + version: 2.32.5 + sha256: 2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 requires_dist: - charset-normalizer>=2,<4 - idna>=2.5,<4 @@ -2766,16 +2817,15 @@ packages: - certifi>=2017.4.17 - pysocks>=1.5.6,!=1.5.7 ; extra == 'socks' - chardet>=3.0.2,<6 ; extra == 'use-chardet-on-py3' - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl name: rich - version: 14.0.0 - sha256: 1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0 + version: 14.1.0 + sha256: 536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f requires_dist: - ipywidgets>=7.5.1,<9 ; extra == 'jupyter' - markdown-it-py>=2.2.0 - pygments>=2.13.0,<3.0.0 - - typing-extensions>=4.0.0,<5.0 ; python_full_version < '3.11' requires_python: '>=3.8.0' - pypi: https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl name: roman-numerals-py @@ -2787,10 +2837,10 @@ packages: - pyright==1.1.394 ; extra == 'lint' - pytest>=8 ; extra == 'test' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/05/7c/8abb70f9017a231c6c961a8941403ed6557664c0913e1bf413cbdc039e75/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/03/36/0a14aebbaa26fe7fab4780c76f2239e76cc95a0090bdb25e31d95c492fcd/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl name: rpds-py - version: 0.25.1 - sha256: a49e1d7a4978ed554f095430b89ecc23f42014a50ac385eb0c4d163ce213c325 + version: 0.27.1 + sha256: 2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418 requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl name: rsa @@ -2799,25 +2849,25 @@ packages: requires_dist: - pyasn1>=0.1.3 requires_python: '>=3.6,<4' -- pypi: https://files.pythonhosted.org/packages/04/9e/91fe10ea09e2383f0d0531cf3da765fa01b3b9918e85df92081813259cf4/ruamel.yaml-0.18.12-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/d1/e5/f2a0621f1781b76a38194acae72f01e37b1941470407345b6e8653ad7640/ruamel.yaml-0.18.15-py3-none-any.whl name: ruamel-yaml - version: 0.18.12 - sha256: 790ba4c48b6a6e6b12b532a7308779eb12d2aaab3a80fdb8389216f28ea2b287 + version: 0.18.15 + sha256: 148f6488d698b7a5eded5ea793a025308b25eca97208181b6a026037f391f701 requires_dist: - ruamel-yaml-clib>=0.2.7 ; python_full_version < '3.14' and platform_python_implementation == 'CPython' - ruamel-yaml-jinja2>=0.2 ; extra == 'jinja2' - ryd ; extra == 'docs' - mercurial>5.7 ; extra == 'docs' - requires_python: '>=3.7' + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/1f/8f/ecfbe2123ade605c49ef769788f79c38ddb1c8fa81e01f4dbf5cf1a44b16/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl name: ruamel-yaml-clib version: 0.2.12 sha256: 0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/0e/2c/1e364cc92970075d7d04c69c928430b23e43a433f044474f57e425cbed37/ruff-0.11.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/80/22/3c6cef96627f89b344c933781ed38329bfb87737aa438f15da95907cbfd5/ruff-0.12.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl name: ruff - version: 0.11.7 - sha256: 7940665e74e7b65d427b82bffc1e46710ec7f30d58b4b2d5016e3f0321436502 + version: 0.12.11 + sha256: 4fc58f9266d62c6eccc75261a665f26b4ef64840887fc6cbc552ce5b29f96cc8 requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl name: semver @@ -2903,15 +2953,16 @@ packages: version: 1.3.1 sha256: 2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl name: snowballstemmer - version: 2.2.0 - sha256: c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a -- pypi: https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl + version: 3.0.1 + sha256: 6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064 + requires_python: '!=3.0.*,!=3.1.*,!=3.2.*' +- pypi: https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl name: soupsieve - version: '2.7' - sha256: 6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4 - requires_python: '>=3.8' + version: '2.8' + sha256: 0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/8d/fd/42a1720542199ae6ff0f9c37bbd55dd3033ddd7bbe00d68cde09d6824887/sparse-0.17.0-py2.py3-none-any.whl name: sparse version: 0.17.0 @@ -2991,10 +3042,10 @@ packages: - setuptools>=70.0 ; extra == 'test' - typing-extensions>=4.9 ; extra == 'test' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/14/2f/bc5bed0677ae00b9ca7919968ea675e2f696b6b20f1648262f26a7a6c6b4/sphinx_autodoc_typehints-3.1.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/f7/c7/8aab362e86cbf887e58be749a78d20ad743e1eb2c73c2b13d4761f39a104/sphinx_autodoc_typehints-3.2.0-py3-none-any.whl name: sphinx-autodoc-typehints - version: 3.1.0 - sha256: 67bdee7e27ba943976ce92ebc5647a976a7a08f9f689a826c54617b96a423913 + version: 3.2.0 + sha256: 884b39be23b1d884dcc825d4680c9c6357a476936e3b381a67ae80091984eb49 requires_dist: - sphinx>=8.2 - furo>=2024.8.6 ; extra == 'docs' @@ -3098,10 +3149,10 @@ packages: - sphinx>=5 ; extra == 'standalone' - pytest ; extra == 'test' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/5e/51/5ba9ea3246ea068630acf35a6ba0d181e99f1af1afd17e159eac7e8bc2b8/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/dc/29/11ae2c2b981de60187f7cbc84277d9d21f101093d1b2e945c63774477aba/sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl name: sqlalchemy - version: 2.0.41 - sha256: dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a + version: 2.0.43 + sha256: 9c5a9da957c56e43d72126a3f5845603da00e0293720b03bde0aacffcf2dc04f requires_dist: - importlib-metadata ; python_full_version < '3.8' - greenlet>=1 ; (python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64') @@ -3178,13 +3229,13 @@ packages: - pytest ; extra == 'tests' - mypy>=1.4 ; extra == 'typing' requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/ce/fd/901cfa59aaa5b30a99e16876f11abe38b59a1a2c51ffb3d7142bb6089069/starlette-0.47.3-py3-none-any.whl name: starlette - version: 0.46.2 - sha256: 595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35 + version: 0.47.3 + sha256: 89c0778ca62a76b826101e7c709e70680a1699ca7da6b44d38eb0a7e61fe4b51 requires_dist: - anyio>=3.6.2,<5 - - typing-extensions>=3.10.0 ; python_full_version < '3.10' + - typing-extensions>=4.10.0 ; python_full_version < '3.13' - httpx>=0.27.0,<0.29.0 ; extra == 'full' - itsdangerous ; extra == 'full' - jinja2 ; extra == 'full' @@ -3202,10 +3253,10 @@ packages: - tornado>=4.5 ; extra == 'test' - typeguard ; extra == 'test' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/3a/d8/1ba8f32bfc9cb69e37edeca93738e883f478fbe84ae401f72c0d8d507841/tifffile-2025.6.11-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/56/b3/23eec760215910609914dd99aba23ce1c72a3bcbe046ee44f45adf740452/tifffile-2025.8.28-py3-none-any.whl name: tifffile - version: 2025.6.11 - sha256: 32effb78b10b3a283eb92d4ebf844ae7e93e151458b0412f38518b4e6d2d7542 + version: 2025.8.28 + sha256: b274a6d9eeba65177cf7320af25ef38ecf910b3369ac6bc494a94a3f6bd99c78 requires_dist: - numpy - imagecodecs>=2024.12.30 ; extra == 'codecs' @@ -3240,10 +3291,10 @@ packages: - xarray ; extra == 'test' - zarr>=3 ; extra == 'test' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/37/63/383de8d9400a52d55683940387b160efb4f6b35bfb7645a443008780e2a9/tiled-0.1.0b29-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/de/a1/815d1083065462f4947759bbb6fe60456bd9eb944cb862c6ed7dba7fe6f2/tiled-0.1.0b39-py3-none-any.whl name: tiled - version: 0.1.0b29 - sha256: 61623955309abed0fe3cfb834899a9f4a3e41e28e47f1f84fd98d88cddfa06d7 + version: 0.1.0b39 + sha256: 74260b7bc1358765cdec979fb06d8c86be4b1a1ee6d88815256d977c14a922ec requires_dist: - httpx>=0.20.0,!=0.23.1 - json-merge-patch @@ -3282,19 +3333,21 @@ packages: - jmespath ; extra == 'all' - lz4 ; extra == 'all' - ndindex ; extra == 'all' - - numcodecs<0.16.0 ; extra == 'all' + - numba>=0.59.0 ; extra == 'all' + - numcodecs ; extra == 'all' - numpy ; extra == 'all' - openpyxl ; extra == 'all' - packaging ; extra == 'all' - pandas ; extra == 'all' - pillow ; extra == 'all' - prometheus-client ; extra == 'all' - - pyarrow ; extra == 'all' + - pyarrow>=14.0.1 ; extra == 'all' - python-dateutil ; extra == 'all' - python-jose[cryptography] ; extra == 'all' - python-multipart ; extra == 'all' + - redis ; extra == 'all' - rich ; extra == 'all' - - sparse ; extra == 'all' + - sparse>=0.15.5 ; extra == 'all' - sqlalchemy[asyncio]>=2 ; extra == 'all' - stamina ; extra == 'all' - starlette>=0.38.0 ; extra == 'all' @@ -3302,7 +3355,7 @@ packages: - uvicorn[standard] ; extra == 'all' - watchfiles ; extra == 'all' - xarray ; extra == 'all' - - zarr<3 ; extra == 'all' + - zarr ; extra == 'all' - zstandard ; extra == 'all' - dask[array] ; extra == 'array' - numpy ; extra == 'array' @@ -3313,13 +3366,15 @@ packages: - entrypoints ; extra == 'client' - lz4 ; extra == 'client' - ndindex ; extra == 'client' + - numba>=0.59.0 ; extra == 'client' - numpy ; extra == 'client' - pandas ; extra == 'client' - - pyarrow ; extra == 'client' + - pyarrow>=14.0.1 ; extra == 'client' - rich ; extra == 'client' - - sparse ; extra == 'client' + - sparse>=0.15.5 ; extra == 'client' - stamina ; extra == 'client' - watchfiles ; extra == 'client' + - websockets ; extra == 'client' - xarray ; extra == 'client' - zstandard ; extra == 'client' - blosc2 ; python_full_version >= '3.10' and extra == 'compression' @@ -3327,21 +3382,23 @@ packages: - zstandard ; extra == 'compression' - dask[dataframe] ; extra == 'dataframe' - pandas ; extra == 'dataframe' - - pyarrow ; extra == 'dataframe' + - pyarrow>=14.0.1 ; extra == 'dataframe' + - aiohttp ; extra == 'dev' - coverage ; extra == 'dev' - flake8 ; extra == 'dev' - importlib-resources ; python_full_version < '3.9' and extra == 'dev' - ipython ; extra == 'dev' - ldap3 ; extra == 'dev' + - locust ; extra == 'dev' - matplotlib ; extra == 'dev' - mistune ; extra == 'dev' - myst-parser ; extra == 'dev' - numpydoc ; extra == 'dev' - pre-commit ; extra == 'dev' + - pytest ; extra == 'dev' - pytest-asyncio ; extra == 'dev' - pytest-mock ; extra == 'dev' - pytest-rerunfailures ; extra == 'dev' - - pytest<8 ; extra == 'dev' - sphinx!=4.1.0,!=4.1.1,!=4.1.2,!=4.2.0 ; extra == 'dev' - sphinx-click ; extra == 'dev' - sphinx-copybutton ; extra == 'dev' @@ -3357,6 +3414,7 @@ packages: - rich ; extra == 'minimal-client' - stamina ; extra == 'minimal-client' - watchfiles ; extra == 'minimal-client' + - websockets ; extra == 'minimal-client' - aiofiles ; extra == 'minimal-server' - aiosqlite ; extra == 'minimal-server' - alembic ; extra == 'minimal-server' @@ -3368,16 +3426,17 @@ packages: - fastapi>=0.115.8 ; extra == 'minimal-server' - jinja2 ; extra == 'minimal-server' - jmespath ; extra == 'minimal-server' - - numcodecs<0.16.0 ; extra == 'minimal-server' + - numcodecs ; extra == 'minimal-server' - packaging ; extra == 'minimal-server' - prometheus-client ; extra == 'minimal-server' - python-dateutil ; extra == 'minimal-server' - python-jose[cryptography] ; extra == 'minimal-server' - python-multipart ; extra == 'minimal-server' + - redis ; extra == 'minimal-server' - sqlalchemy[asyncio]>=2 ; extra == 'minimal-server' - starlette>=0.38.0 ; extra == 'minimal-server' - uvicorn[standard] ; extra == 'minimal-server' - - zarr<3 ; extra == 'minimal-server' + - zarr ; extra == 'minimal-server' - adbc-driver-manager ; extra == 'server' - adbc-driver-postgresql ; extra == 'server' - adbc-driver-sqlite ; extra == 'server' @@ -3403,44 +3462,49 @@ packages: - jmespath ; extra == 'server' - lz4 ; extra == 'server' - ndindex ; extra == 'server' - - numcodecs<0.16.0 ; extra == 'server' + - numba>=0.59.0 ; extra == 'server' + - numcodecs ; extra == 'server' - numpy ; extra == 'server' - openpyxl ; extra == 'server' - packaging ; extra == 'server' - pandas ; extra == 'server' - pillow ; extra == 'server' - prometheus-client ; extra == 'server' - - pyarrow ; extra == 'server' + - pyarrow>=14.0.1 ; extra == 'server' - python-dateutil ; extra == 'server' - python-jose[cryptography] ; extra == 'server' - python-multipart ; extra == 'server' - - sparse ; extra == 'server' + - redis ; extra == 'server' + - sparse>=0.15.5 ; extra == 'server' - sqlalchemy[asyncio]>=2 ; extra == 'server' - starlette>=0.38.0 ; extra == 'server' - tifffile ; extra == 'server' - uvicorn[standard] ; extra == 'server' + - websockets ; extra == 'server' - xarray ; extra == 'server' - - zarr<3 ; extra == 'server' + - zarr ; extra == 'server' - zstandard ; extra == 'server' - ndindex ; extra == 'sparse' - - pyarrow ; extra == 'sparse' - - sparse ; extra == 'sparse' + - numba>=0.59.0 ; extra == 'sparse' + - pyarrow>=14.0.1 ; extra == 'sparse' + - sparse>=0.15.5 ; extra == 'sparse' - dask[array] ; extra == 'xarray' - pandas ; extra == 'xarray' - pyarrow ; extra == 'xarray' - xarray ; extra == 'xarray' requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda - sha256: e0569c9caa68bf476bead1bed3d79650bb080b532c64a4af7d8ca286c08dea4e - md5: d453b98d9c83e71da0741bb0ff4d76bc +- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda + sha256: a84ff687119e6d8752346d1d408d5cf360dee0badd487a472aa8ddedfdc219e1 + md5: a0116df4f4ed05c303811a837d5b39d8 depends: - - libgcc-ng >=12 - - libzlib >=1.2.13,<2.0.0a0 + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libzlib >=1.3.1,<2.0a0 license: TCL license_family: BSD purls: [] - size: 3318875 - timestamp: 1699202167581 + size: 3285204 + timestamp: 1748387766691 - pypi: https://files.pythonhosted.org/packages/03/98/eb27cc78ad3af8e302c9d8ff4977f5026676e130d28dd7578132a457170c/toolz-1.0.0-py3-none-any.whl name: toolz version: 1.0.0 @@ -3477,21 +3541,21 @@ packages: - pytest-mypy-testing ; extra == 'test' - pytest>=7.0,<8.2 ; extra == 'test' requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/ca/e8/b3d537470e8404659a6335e7af868e90657efb73916ef31ddf3d8b9cb237/typer-0.17.3-py3-none-any.whl name: typer - version: 0.16.0 - sha256: 1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855 + version: 0.17.3 + sha256: 643919a79182ab7ac7581056d93c6a2b865b026adf2872c4d02c72758e6f095b requires_dist: - click>=8.0.0 - typing-extensions>=3.7.4.3 - shellingham>=1.3.0 - rich>=10.11.0 requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl name: typing-extensions - version: 4.13.2 - sha256: a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c - requires_python: '>=3.8' + version: 4.15.0 + sha256: f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl name: typing-inspection version: 0.4.1 @@ -3511,10 +3575,10 @@ packages: purls: [] size: 122968 timestamp: 1742727099393 -- pypi: https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl name: urllib3 - version: 2.4.0 - sha256: 4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813 + version: 2.5.0 + sha256: e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc requires_dist: - brotli>=1.0.9 ; platform_python_implementation == 'CPython' and extra == 'brotli' - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'brotli' @@ -3555,15 +3619,16 @@ packages: - pyopenssl~=23.0.0 ; extra == 'test' - mypy>=0.800 ; extra == 'test' requires_python: '>=3.8.0' -- pypi: https://files.pythonhosted.org/packages/4c/ed/3cfeb48175f0671ec430ede81f628f9fb2b1084c9064ca67ebe8c0ed6a05/virtualenv-20.30.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl name: virtualenv - version: 20.30.0 - sha256: e34302959180fca3af42d1800df014b35019490b119eba981af27f2fa486e5d6 + version: 20.34.0 + sha256: 341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026 requires_dist: - distlib>=0.3.7,<1 - filelock>=3.12.2,<4 - importlib-metadata>=6.6 ; python_full_version < '3.8' - platformdirs>=3.9.1,<5 + - typing-extensions>=4.13.2 ; python_full_version < '3.11' - furo>=2023.7.26 ; extra == 'docs' - proselint>=0.13 ; extra == 'docs' - sphinx>=7.1.2,!=7.3 ; extra == 'docs' @@ -3602,37 +3667,33 @@ packages: version: 15.0.1 sha256: 0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8 requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl - name: wrapt - version: 1.17.2 - sha256: 8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/82/8a/6b50c1dd2260d407c1a499d47cf829f59f07007e0dcebafdabb24d1d26a5/xarray-2025.6.1-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/e6/c8/0f8db9d9478de8d70cbcae2056588401e26168e269d6d9919bf2ecb01f78/xarray-2025.8.0-py3-none-any.whl name: xarray - version: 2025.6.1 - sha256: 8b988b47f67a383bdc3b04c5db475cd165e580134c1f1943d52aee4a9c97651b + version: 2025.8.0 + sha256: 1c454f32b38c93df68e450238c9473fe21248b8572d42ddd58c5170bb30934ee requires_dist: - - numpy>=1.24 - - packaging>=23.2 - - pandas>=2.1 - - scipy ; extra == 'accel' + - numpy>=1.26 + - packaging>=24.1 + - pandas>=2.2 + - scipy>=1.13 ; extra == 'accel' - bottleneck ; extra == 'accel' - - numbagg ; extra == 'accel' - - numba>=0.54 ; extra == 'accel' - - flox ; extra == 'accel' + - numbagg>=0.8 ; extra == 'accel' + - numba>=0.59 ; extra == 'accel' + - flox>=0.9 ; extra == 'accel' - opt-einsum ; extra == 'accel' + - numpy<2.3 ; extra == 'accel' - xarray[accel,etc,io,parallel,viz] ; extra == 'complete' - - netcdf4 ; extra == 'io' + - netcdf4>=1.6.0 ; extra == 'io' - h5netcdf ; extra == 'io' - - scipy ; extra == 'io' - - pydap ; python_full_version < '3.10' and extra == 'io' - - zarr ; extra == 'io' + - pydap ; extra == 'io' + - scipy>=1.13 ; extra == 'io' + - zarr>=2.18 ; extra == 'io' - fsspec ; extra == 'io' - cftime ; extra == 'io' - pooch ; extra == 'io' - - sparse ; extra == 'etc' + - sparse>=0.15 ; extra == 'etc' - dask[complete] ; extra == 'parallel' - - cartopy ; extra == 'viz' + - cartopy>=0.23 ; extra == 'viz' - matplotlib ; extra == 'viz' - nc-time-axis ; extra == 'viz' - seaborn ; extra == 'viz' @@ -3652,44 +3713,61 @@ packages: - types-python-dateutil ; extra == 'types' - types-pytz ; extra == 'types' - types-setuptools ; extra == 'types' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/5e/d8/9ffd8c237b3559945bb52103cf0eed64ea098f7b7f573f8d2962ef27b4b2/zarr-2.18.7-py3-none-any.whl + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/e0/a3/d3d4fd394a10b1256f9dccb2fe0ddd125fc575d7c437b1c70df050f14176/zarr-3.1.2-py3-none-any.whl name: zarr - version: 2.18.7 - sha256: ac3dc4033e9ae4e9d7b5e27c97ea3eaf1003cc0a07f010bd83d5134bf8c4b223 + version: 3.1.2 + sha256: c3e180f53ee0ef91b86f7feff6f9dd381ddd1b512d1a46580530966a493387b6 requires_dist: - - asciitree - - numpy>=1.24 - - fasteners ; sys_platform != 'emscripten' - - numcodecs>=0.10.0,!=0.14.0,!=0.14.1,<0.16 - - notebook ; extra == 'jupyter' - - ipytree>=0.2.2 ; extra == 'jupyter' - - ipywidgets>=8.0.0 ; extra == 'jupyter' - - sphinx ; extra == 'docs' - - sphinx-automodapi ; extra == 'docs' + - donfig>=0.8 + - numcodecs[crc32c]>=0.14 + - numpy>=1.26 + - packaging>=22.0 + - typing-extensions>=4.9 + - astroid<4 ; extra == 'docs' + - numcodecs[msgpack] ; extra == 'docs' + - numpydoc ; extra == 'docs' + - pydata-sphinx-theme ; extra == 'docs' + - pytest ; extra == 'docs' + - rich ; extra == 'docs' + - s3fs>=2023.10.0 ; extra == 'docs' + - sphinx-autoapi==3.4.0 ; extra == 'docs' + - sphinx-autobuild>=2021.3.14 ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' - sphinx-design ; extra == 'docs' - sphinx-issues ; extra == 'docs' - - sphinx-copybutton ; extra == 'docs' - - pydata-sphinx-theme ; extra == 'docs' - - numpydoc ; extra == 'docs' - - numcodecs[msgpack]!=0.14.0,!=0.14.1,<0.16 ; extra == 'docs' - - pytest-doctestplus ; extra == 'docs' + - sphinx-reredirects ; extra == 'docs' + - sphinx==8.1.3 ; extra == 'docs' + - towncrier ; extra == 'docs' + - cupy-cuda12x ; extra == 'gpu' + - rich ; extra == 'optional' + - universal-pathlib ; extra == 'optional' + - fsspec>=2023.10.0 ; extra == 'remote' + - obstore>=0.5.1 ; extra == 'remote' + - botocore ; extra == 'remote-tests' + - fsspec>=2023.10.0 ; extra == 'remote-tests' + - moto[s3,server] ; extra == 'remote-tests' + - obstore>=0.5.1 ; extra == 'remote-tests' + - requests ; extra == 'remote-tests' + - s3fs>=2023.10.0 ; extra == 'remote-tests' + - coverage>=7.10 ; extra == 'test' + - hypothesis ; extra == 'test' + - mypy ; extra == 'test' + - packaging ; extra == 'test' + - pytest-accept ; extra == 'test' + - pytest-asyncio ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-xdist ; extra == 'test' + - pytest<8.4 ; extra == 'test' + - rich ; extra == 'test' + - tomlkit ; extra == 'test' + - uv ; extra == 'test' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl name: zipp - version: 3.21.0 - sha256: ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931 + version: 3.23.0 + sha256: 071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e requires_dist: - - pytest-checkdocs>=2.4 ; extra == 'check' - - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' - - pytest-cov ; extra == 'cover' - - sphinx>=3.5 ; extra == 'doc' - - jaraco-packaging>=9.3 ; extra == 'doc' - - rst-linker>=1.9 ; extra == 'doc' - - furo ; extra == 'doc' - - sphinx-lint ; extra == 'doc' - - jaraco-tidelift>=1.4 ; extra == 'doc' - - pytest-enabler>=2.2 ; extra == 'enabler' - pytest>=6,!=8.1.* ; extra == 'test' - jaraco-itertools ; extra == 'test' - jaraco-functools ; extra == 'test' @@ -3697,14 +3775,22 @@ packages: - big-o ; extra == 'test' - pytest-ignore-flaky ; extra == 'test' - jaraco-test ; extra == 'test' - - importlib-resources ; python_full_version < '3.9' and extra == 'test' + - sphinx>=3.5 ; extra == 'doc' + - jaraco-packaging>=9.3 ; extra == 'doc' + - rst-linker>=1.9 ; extra == 'doc' + - furo ; extra == 'doc' + - sphinx-lint ; extra == 'doc' + - jaraco-tidelift>=1.4 ; extra == 'doc' + - pytest-checkdocs>=2.4 ; extra == 'check' + - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' + - pytest-cov ; extra == 'cover' + - pytest-enabler>=2.2 ; extra == 'enabler' - pytest-mypy ; extra == 'type' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/fa/18/89ac62eac46b69948bf35fcd90d37103f38722968e2981f752d69081ec4d/zstandard-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/d9/3d/02aba892327a67ead8cba160ee835cfa1fc292a9dcb763639e30c07da58b/zstandard-0.24.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl name: zstandard - version: 0.23.0 - sha256: 6f77fa49079891a4aab203d0b1744acc85577ed16d767b52fc089d83faf8d8ed + version: 0.24.0 + sha256: 1b14bc92af065d0534856bf1b30fc48753163ea673da98857ea4932be62079b1 requires_dist: - - cffi>=1.17 ; platform_python_implementation == 'PyPy' - - cffi>=1.17 ; extra == 'cffi' - requires_python: '>=3.8' + - cffi>=1.17 ; python_full_version >= '3.13' and platform_python_implementation != 'PyPy' and extra == 'cffi' + requires_python: '>=3.9' From 9cfe709dd804c46e3ec545e3c4b50bf353b8bbf0 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Tue, 2 Sep 2025 10:01:25 -0400 Subject: [PATCH 42/46] Update dependencies and fix tests with new ophyd-async version --- src/cditools/eiger_async.py | 77 +++++++++++++++---------------------- 1 file changed, 32 insertions(+), 45 deletions(-) diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index d3b752b4..e15c159c 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -5,11 +5,11 @@ from __future__ import annotations import asyncio -from collections.abc import AsyncGenerator, AsyncIterator, Sequence +from collections.abc import AsyncGenerator, AsyncIterator, Sequence, Iterator from logging import getLogger from pathlib import Path from typing import Annotated as A -from typing import Any, Literal +from typing import Any, cast from urllib.parse import urlunparse import numpy as np # type: ignore[import-not-found] @@ -18,11 +18,13 @@ ComposeStreamResource, ComposeStreamResourceBundle, DataKey, # type: ignore[import-untyped] + StreamRange, + StreamResource, + StreamDatum, ) from ophyd_async.core import ( DetectorTrigger, HDFDatasetDescription, - HDFDocumentComposer, PathInfo, PathProvider, SignalDatatypeT, @@ -36,11 +38,10 @@ ADBaseController, ADBaseDatasetDescriber, ADBaseIO, - ADFileWriteMode, ADImageMode, ADWriter, AreaDetector, - NDArrayBaseIO, + NDFileIO, NDPluginBaseIO, ) from ophyd_async.epics.signal import PvSuffix @@ -48,20 +49,15 @@ logger = getLogger(__name__) -# TODO: Port to ophyd-async -# ============================================================================== -class HDFDatasetDescription2(HDFDatasetDescription): - join_method: Literal["stack", "concat"] = "concat" - - -class HDFDocumentComposer2(HDFDocumentComposer): +class EigerDocumentComposer: def __init__( self, full_file_name: Path, - datasets: list[HDFDatasetDescription2], + datasets: list[HDFDatasetDescription], + last_emitted_index: int = 0, hostname: str = "localhost", ) -> None: - self._last_emitted = 0 + self._last_emitted = last_emitted_index self._hostname = hostname uri = urlunparse( ( @@ -82,7 +78,6 @@ def __init__( parameters={ "dataset": ds.dataset, "chunk_shape": ds.chunk_shape, - "join_method": ds.join_method, }, uid=None, validate=True, @@ -90,30 +85,19 @@ def __init__( for ds in datasets ] + def stream_resources(self) -> Iterator[StreamResource]: + for bundle in self._bundles: + yield bundle.stream_resource_doc -# ============================================================================== - - -# TODO: Port to ophyd-async (https://github.com/bluesky/ophyd-async/issues/961) -# ============================================================================== -class NDFileIO(NDArrayBaseIO): - file_path: A[SignalRW[str], PvSuffix.rbv("FilePath")] - file_name: A[SignalRW[str], PvSuffix.rbv("FileName")] - file_path_exists: A[SignalR[bool], PvSuffix("FilePathExists_RBV")] - file_template: A[SignalRW[str], PvSuffix.rbv("FileTemplate")] - full_file_name: A[SignalR[str], PvSuffix("FullFileName_RBV")] - file_number: A[SignalRW[int], PvSuffix("FileNumber")] - auto_increment: A[SignalRW[bool], PvSuffix("AutoIncrement")] - file_write_mode: A[SignalRW[ADFileWriteMode], PvSuffix.rbv("FileWriteMode")] - num_capture: A[SignalRW[int], PvSuffix.rbv("NumCapture")] - num_captured: A[SignalR[int], PvSuffix("NumCaptured_RBV")] - capture: A[SignalRW[bool], PvSuffix.rbv("Capture")] - array_size0: A[SignalR[int], PvSuffix("ArraySize0")] - array_size1: A[SignalR[int], PvSuffix("ArraySize1")] - create_directory: A[SignalRW[int], PvSuffix("CreateDirectory")] - - -# ============================================================================== + def stream_data(self, indices_written: int) -> Iterator[StreamDatum]: + if indices_written > self._last_emitted: + indices: StreamRange = { + "start": self._last_emitted, + "stop": indices_written, + } + self._last_emitted = indices_written + for bundle in self._bundles: + yield bundle.compose_stream_datum(indices) class EigerTriggerMode(StrictEnum): @@ -376,7 +360,7 @@ def __init__( ) self._file_info: PathInfo | None = None - self._datasets: list[HDFDatasetDescription2] = [] + self._datasets: list[HDFDatasetDescription] = [] self._master_file_path_cache: list[Path] = [] async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataKey]: @@ -500,14 +484,18 @@ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataK # ), # ] + if any(s is None for s in detector_shape): + chunk_shape = (1,) + else: + chunk_shape = cast(tuple[int, ...], (1, *detector_shape)) frame_datasets = [ - HDFDatasetDescription2( + HDFDatasetDescription( data_key=f"{name}_image", dataset=f"entry/data/data_{1:06d}", shape=(exposures_per_event, *detector_shape), # Always write as uint32 dtype_numpy=np.dtype(np.uint32).str, - chunk_shape=(1, *detector_shape), + chunk_shape=chunk_shape, ) ] @@ -536,7 +524,7 @@ async def _master_file_path(self) -> Path | None: ) return None sequence_id = await self.fileio.sequence_id.get_value() - return ( + return Path( self._file_info.directory_path / f"{self._file_info.filename}_{sequence_id}_master.h5" ) @@ -554,12 +542,11 @@ async def collect_stream_docs( # Eiger generates a new master file for each trigger # so we need to create a new composer with a new # master file path - composer = HDFDocumentComposer2( + composer = EigerDocumentComposer( master_file_path, self._datasets, + last_emitted_index=indices_written - 1, ) - # TODO: Make public - composer._last_emitted = indices_written - 1 # type: ignore[reportPrivateUsage] # For later validation self._master_file_path_cache.append(master_file_path) From ff4121f73fad6b22110f4492eb121b72fa94ffc5 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Tue, 2 Sep 2025 10:08:11 -0400 Subject: [PATCH 43/46] Remove pyright type checking; Fix ruff linting errors --- .pre-commit-config.yaml | 16 ++++++++-------- src/cditools/eiger_async.py | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 16038706..66389482 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,14 +41,14 @@ repos: args: ["--fix", "--show-fixes"] - id: ruff-format - - repo: local - hooks: - - id: pyright - name: pyright - entry: pyright - language: system - types: [python] - files: src|tests + # - repo: local + # hooks: + # - id: pyright + # name: pyright + # entry: pyright + # language: system + # types: [python] + # files: src|tests - repo: local hooks: diff --git a/src/cditools/eiger_async.py b/src/cditools/eiger_async.py index e15c159c..65bf5bb5 100644 --- a/src/cditools/eiger_async.py +++ b/src/cditools/eiger_async.py @@ -5,7 +5,7 @@ from __future__ import annotations import asyncio -from collections.abc import AsyncGenerator, AsyncIterator, Sequence, Iterator +from collections.abc import AsyncGenerator, AsyncIterator, Iterator, Sequence from logging import getLogger from pathlib import Path from typing import Annotated as A @@ -18,9 +18,9 @@ ComposeStreamResource, ComposeStreamResourceBundle, DataKey, # type: ignore[import-untyped] + StreamDatum, StreamRange, StreamResource, - StreamDatum, ) from ophyd_async.core import ( DetectorTrigger, From 467c3efa796ee9556b201f822c9be2b65b858c84 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Mon, 20 Oct 2025 09:59:05 -0400 Subject: [PATCH 44/46] Remove 'streams' from tiled access --- tests/test_eiger_async.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_eiger_async.py b/tests/test_eiger_async.py index cec1778e..068aebb3 100644 --- a/tests/test_eiger_async.py +++ b/tests/test_eiger_async.py @@ -569,7 +569,7 @@ async def _write_file(value: bool, wait: bool) -> None: uid = RE(bp.count([mock_eiger_detector])) assert uid is not None assert ( - tiled_client.values().last()["streams"]["primary"]["test_eiger_image"].read() + tiled_client.values().last()["primary"]["test_eiger_image"].read() is not None ) assert tiled_client.values().last()["streams"]["primary"][ From c566dbbd10d08a5529ca97715a6041119d285f71 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Mon, 20 Oct 2025 10:10:29 -0400 Subject: [PATCH 45/46] pre-commit --- tests/test_eiger_async.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_eiger_async.py b/tests/test_eiger_async.py index 068aebb3..d9945dba 100644 --- a/tests/test_eiger_async.py +++ b/tests/test_eiger_async.py @@ -569,8 +569,7 @@ async def _write_file(value: bool, wait: bool) -> None: uid = RE(bp.count([mock_eiger_detector])) assert uid is not None assert ( - tiled_client.values().last()["primary"]["test_eiger_image"].read() - is not None + tiled_client.values().last()["primary"]["test_eiger_image"].read() is not None ) assert tiled_client.values().last()["streams"]["primary"][ "test_eiger_image" From 0805bd9e152645d76005dfc03d06682929b7b502 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Mon, 20 Oct 2025 10:16:40 -0400 Subject: [PATCH 46/46] Remove all uses of 'streams' in tests --- tests/test_eiger_async.py | 125 ++++++++++++++++++-------------------- 1 file changed, 59 insertions(+), 66 deletions(-) diff --git a/tests/test_eiger_async.py b/tests/test_eiger_async.py index d9945dba..da60bd82 100644 --- a/tests/test_eiger_async.py +++ b/tests/test_eiger_async.py @@ -571,105 +571,102 @@ async def _write_file(value: bool, wait: bool) -> None: assert ( tiled_client.values().last()["primary"]["test_eiger_image"].read() is not None ) - assert tiled_client.values().last()["streams"]["primary"][ - "test_eiger_image" - ].shape == ( + assert tiled_client.values().last()["primary"]["test_eiger_image"].shape == ( 1, 2048, 2048, ) assert ( - tiled_client.values().last()["streams"]["primary"]["test_eiger_image"].dtype - == np.uint32 + tiled_client.values().last()["primary"]["test_eiger_image"].dtype == np.uint32 ) # TODO: Add these when empty shape datasets are supported by tiled - # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_x_pixel_size"].read() is not None - # assert tiled_client.values().last()["streams"]["primary"][ + # assert tiled_client.values().last()["primary"]["test_eiger_x_pixel_size"].read() is not None + # assert tiled_client.values().last()["primary"][ # "test_eiger_x_pixel_size" # ].shape == () # assert ( # tiled_client.values() - # .last()["streams"]["primary"]["test_eiger_x_pixel_size"] + # .last()["primary"]["test_eiger_x_pixel_size"] # .dtype # == np.float32 # ) - # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_y_pixel_size"].read() is not None - # assert tiled_client.values().last()["streams"]["primary"][ + # assert tiled_client.values().last()["primary"]["test_eiger_y_pixel_size"].read() is not None + # assert tiled_client.values().last()["primary"][ # "test_eiger_y_pixel_size" # ].shape == () # assert ( # tiled_client.values() - # .last()["streams"]["primary"]["test_eiger_y_pixel_size"] + # .last()["primary"]["test_eiger_y_pixel_size"] # .dtype # == np.float32 # ) - # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_detector_distance"].read() is not None - # assert tiled_client.values().last()["streams"]["primary"][ + # assert tiled_client.values().last()["primary"]["test_eiger_detector_distance"].read() is not None + # assert tiled_client.values().last()["primary"][ # "test_eiger_detector_distance" # ].shape == () # assert ( # tiled_client.values() - # .last()["streams"]["primary"]["test_eiger_detector_distance"] + # .last()["primary"]["test_eiger_detector_distance"] # .dtype # == np.float32 # ) - # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_incident_wavelength"].read() is not None - # assert tiled_client.values().last()["streams"]["primary"][ + # assert tiled_client.values().last()["primary"]["test_eiger_incident_wavelength"].read() is not None + # assert tiled_client.values().last()["primary"][ # "test_eiger_incident_wavelength" # ].shape == () # assert ( # tiled_client.values() - # .last()["streams"]["primary"]["test_eiger_incident_wavelength"] + # .last()["primary"]["test_eiger_incident_wavelength"] # .dtype # == np.float32 # ) - # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_frame_time"].read() is not None - # assert tiled_client.values().last()["streams"]["primary"][ + # assert tiled_client.values().last()["primary"]["test_eiger_frame_time"].read() is not None + # assert tiled_client.values().last()["primary"][ # "test_eiger_frame_time" # ].shape == () # assert ( # tiled_client.values() - # .last()["streams"]["primary"]["test_eiger_frame_time"] + # .last()["primary"]["test_eiger_frame_time"] # .dtype # == np.float32 # ) - # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_beam_center_x"].read() is not None - # assert tiled_client.values().last()["streams"]["primary"][ + # assert tiled_client.values().last()["primary"]["test_eiger_beam_center_x"].read() is not None + # assert tiled_client.values().last()["primary"][ # "test_eiger_beam_center_x" # ].shape == () # assert ( # tiled_client.values() - # .last()["streams"]["primary"]["test_eiger_beam_center_x"] + # .last()["primary"]["test_eiger_beam_center_x"] # .dtype # == np.float32 # ) - # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_beam_center_y"].read() is not None - # assert tiled_client.values().last()["streams"]["primary"][ + # assert tiled_client.values().last()["primary"]["test_eiger_beam_center_y"].read() is not None + # assert tiled_client.values().last()["primary"][ # "test_eiger_beam_center_y" # ].shape == () # assert ( # tiled_client.values() - # .last()["streams"]["primary"]["test_eiger_beam_center_y"] + # .last()["primary"]["test_eiger_beam_center_y"] # .dtype # == np.float32 # ) - # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_count_time"].read() is not None - # assert tiled_client.values().last()["streams"]["primary"][ + # assert tiled_client.values().last()["primary"]["test_eiger_count_time"].read() is not None + # assert tiled_client.values().last()["primary"][ # "test_eiger_count_time" # ].shape == () # assert ( # tiled_client.values() - # .last()["streams"]["primary"]["test_eiger_count_time"] + # .last()["primary"]["test_eiger_count_time"] # .dtype # == np.float32 # ) - # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_pixel_mask"].read() is not None - # assert tiled_client.values().last()["streams"]["primary"][ + # assert tiled_client.values().last()["primary"]["test_eiger_pixel_mask"].read() is not None + # assert tiled_client.values().last()["primary"][ # "test_eiger_pixel_mask" # ].shape == (2048, 2048) # assert ( # tiled_client.values() - # .last()["streams"]["primary"]["test_eiger_pixel_mask"] + # .last()["primary"]["test_eiger_pixel_mask"] # .dtype # == np.uint32 # ) @@ -680,108 +677,104 @@ async def _write_file(value: bool, wait: bool) -> None: uid = RE(bp.count([mock_eiger_detector], num=10)) assert uid is not None assert ( - tiled_client.values().last()["streams"]["primary"]["test_eiger_image"].read() - is not None + tiled_client.values().last()["primary"]["test_eiger_image"].read() is not None ) - assert tiled_client.values().last()["streams"]["primary"][ - "test_eiger_image" - ].shape == ( + assert tiled_client.values().last()["primary"]["test_eiger_image"].shape == ( 10, 2048, 2048, ) assert ( - tiled_client.values().last()["streams"]["primary"]["test_eiger_image"].dtype - == np.uint32 + tiled_client.values().last()["primary"]["test_eiger_image"].dtype == np.uint32 ) # TODO: Add these when empty shape datasets are supported by tiled - # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_x_pixel_size"].read() is not None - # assert tiled_client.values().last()["streams"]["primary"][ + # assert tiled_client.values().last()["primary"]["test_eiger_x_pixel_size"].read() is not None + # assert tiled_client.values().last()["primary"][ # "test_eiger_x_pixel_size" # ].shape == (10,) # assert ( # tiled_client.values() - # .last()["streams"]["primary"]["test_eiger_x_pixel_size"] + # .last()["primary"]["test_eiger_x_pixel_size"] # .dtype # == np.float32 # ) - # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_y_pixel_size"].read() is not None - # assert tiled_client.values().last()["streams"]["primary"][ + # assert tiled_client.values().last()["primary"]["test_eiger_y_pixel_size"].read() is not None + # assert tiled_client.values().last()["primary"][ # "test_eiger_y_pixel_size" # ].shape == (10,) # assert ( # tiled_client.values() - # .last()["streams"]["primary"]["test_eiger_y_pixel_size"] + # .last()["primary"]["test_eiger_y_pixel_size"] # .dtype # == np.float32 # ) - # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_detector_distance"].read() is not None - # assert tiled_client.values().last()["streams"]["primary"][ + # assert tiled_client.values().last()["primary"]["test_eiger_detector_distance"].read() is not None + # assert tiled_client.values().last()["primary"][ # "test_eiger_detector_distance" # ].shape == (10,) # assert ( # tiled_client.values() - # .last()["streams"]["primary"]["test_eiger_detector_distance"] + # .last()["primary"]["test_eiger_detector_distance"] # .dtype # == np.float32 # ) - # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_incident_wavelength"].read() is not None - # assert tiled_client.values().last()["streams"]["primary"][ + # assert tiled_client.values().last()["primary"]["test_eiger_incident_wavelength"].read() is not None + # assert tiled_client.values().last()["primary"][ # "test_eiger_incident_wavelength" # ].shape == (10,) # assert ( # tiled_client.values() - # .last()["streams"]["primary"]["test_eiger_incident_wavelength"] + # .last()["primary"]["test_eiger_incident_wavelength"] # .dtype # == np.float32 # ) - # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_frame_time"].read() is not None - # assert tiled_client.values().last()["streams"]["primary"][ + # assert tiled_client.values().last()["primary"]["test_eiger_frame_time"].read() is not None + # assert tiled_client.values().last()["primary"][ # "test_eiger_frame_time" # ].shape == (10,) # assert ( # tiled_client.values() - # .last()["streams"]["primary"]["test_eiger_frame_time"] + # .last()["primary"]["test_eiger_frame_time"] # .dtype # == np.float32 # ) - # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_beam_center_x"].read() is not None - # assert tiled_client.values().last()["streams"]["primary"][ + # assert tiled_client.values().last()["primary"]["test_eiger_beam_center_x"].read() is not None + # assert tiled_client.values().last()["primary"][ # "test_eiger_beam_center_x" # ].shape == (10,) # assert ( # tiled_client.values() - # .last()["streams"]["primary"]["test_eiger_beam_center_x"] + # .last()["primary"]["test_eiger_beam_center_x"] # .dtype # == np.float32 # ) - # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_beam_center_y"].read() is not None - # assert tiled_client.values().last()["streams"]["primary"][ + # assert tiled_client.values().last()["primary"]["test_eiger_beam_center_y"].read() is not None + # assert tiled_client.values().last()["primary"][ # "test_eiger_beam_center_y" # ].shape == (10,) # assert ( # tiled_client.values() - # .last()["streams"]["primary"]["test_eiger_beam_center_y"] + # .last()["primary"]["test_eiger_beam_center_y"] # .dtype # == np.float32 # ) - # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_count_time"].read() is not None - # assert tiled_client.values().last()["streams"]["primary"][ + # assert tiled_client.values().last()["primary"]["test_eiger_count_time"].read() is not None + # assert tiled_client.values().last()["primary"][ # "test_eiger_count_time" # ].shape == (10,) # assert ( # tiled_client.values() - # .last()["streams"]["primary"]["test_eiger_count_time"] + # .last()["primary"]["test_eiger_count_time"] # .dtype # == np.float32 # ) - # assert tiled_client.values().last()["streams"]["primary"]["test_eiger_pixel_mask"].read() is not None - # assert tiled_client.values().last()["streams"]["primary"][ + # assert tiled_client.values().last()["primary"]["test_eiger_pixel_mask"].read() is not None + # assert tiled_client.values().last()["primary"][ # "test_eiger_pixel_mask" # ].shape == (10, 2048, 2048) # assert ( # tiled_client.values() - # .last()["streams"]["primary"]["test_eiger_pixel_mask"] + # .last()["primary"]["test_eiger_pixel_mask"] # .dtype # == np.uint32 # )