diff --git a/docs/conf.py b/docs/conf.py index 3ee2ad966..4c36118d1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -83,6 +83,8 @@ # docstring of collections.abc.Callable:1: WARNING: # 'any' reference target not found: self [ref.any] ("any", "self"), + # Inherited from pydantic.BaseModel.__init__, which has no intersphinx mapping + ("any", "ValidationError"), # p4p doesn't have intersphinx mapping ("py:class", "p4p.server.StaticProvider"), ("py:class", "p4p.nt.scalar.NTScalar"), diff --git a/pyproject.toml b/pyproject.toml index f178a375c..23d42ad38 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,8 +5,8 @@ build-backend = "setuptools.build_meta" [project] name = "fastcs" authors = [ - {name = "Martin Gaughran", email = "martin.gaughran@diamond.ac.uk"}, - {name = "Gary Yendell", email = "gary.yendell@diamond.ac.uk"}, + { name = "Martin Gaughran", email = "martin.gaughran@diamond.ac.uk" }, + { name = "Gary Yendell", email = "gary.yendell@diamond.ac.uk" }, ] classifiers = [ "Development Status :: 3 - Alpha", @@ -34,7 +34,10 @@ requires-python = ">=3.11" [project.optional-dependencies] demo = ["tickit~=0.4.3"] -epicsca = ["pvi~=0.12.0", "softioc>=4.5.0"] +epicsca = [ + "pvi~=0.12.0", + "softioc @ git+https://github.com/DiamondLightSource/pythonSoftIOC.git@master", +] epicspva = ["p4p", "pvi~=0.12.0"] epics = ["fastcs[epicsca]", "fastcs[epicspva]"] tango = ["pytango"] diff --git a/src/fastcs/attributes/attr_w.py b/src/fastcs/attributes/attr_w.py index 3e6a4517d..74c9a4828 100644 --- a/src/fastcs/attributes/attr_w.py +++ b/src/fastcs/attributes/attr_w.py @@ -59,6 +59,7 @@ async def put(self, setpoint: DType_T, sync_setpoint: bool = False) -> None: logger.opt(exception=e).error( "Put failed", attribute=self, setpoint=setpoint ) + raise if sync_setpoint: try: diff --git a/src/fastcs/transports/epics/ca/ioc.py b/src/fastcs/transports/epics/ca/ioc.py index 536cdcfa5..7f210c424 100644 --- a/src/fastcs/transports/epics/ca/ioc.py +++ b/src/fastcs/transports/epics/ca/ioc.py @@ -1,8 +1,9 @@ import asyncio from collections import Counter +from collections.abc import Awaitable from typing import Any, Literal -from softioc import builder, softioc +from softioc import alarm, builder, softioc from softioc.asyncio_dispatcher import AsyncioDispatcher from softioc.pythonSoftIoc import RecordWrapper @@ -223,13 +224,14 @@ def _create_and_link_write_pv( attr_name: str, alias: str | None, attribute: AttrW[DType_T], -) -> None: +): pv = f"{pv_prefix}:{pv_name}" async def on_update(value): logger.info("PV put: {pv} = {value}", pv=pv, value=repr(value)) - - await attribute.put(cast_from_epics_type(attribute.datatype, value)) + await _run_and_set_alarm( + record, attribute.put(cast_from_epics_type(attribute.datatype, value)) + ) async def set_setpoint_without_process(value: DType_T): tracer.log_event( @@ -280,8 +282,7 @@ def _create_and_link_command_pv( async def wrapped_method(_: Any): tracer.log_event("Command PV put", topic=method, pv=pv) - - await method.fn() + await _run_and_set_alarm(record, method.fn()) record = builder.Action( f"{pv_prefix}:{pv_name}", @@ -335,3 +336,27 @@ def _add_alias(record: RecordWrapper, alias: str | None): ) else: record.add_alias(alias) + + +def _set_alarm(record: RecordWrapper, alarm_state: int): + record.set( + record.get(), + process=False, + severity=alarm_state, + alarm=alarm_state, + ) + + +async def _run_and_set_alarm(record, coro: Awaitable): + """Await `coro` and update `record`'s alarm state based on the outcome. + + On success, clears the alarm (NO_ALARM). On any exception, raises the + record into MAJOR_ALARM. The exception itself is not re-raised or + logged here, since `AttrW.put` already logs it; this function's only + job is to reflect the outcome in the record's alarm status. + """ + try: + await coro + _set_alarm(record, alarm.NO_ALARM) + except Exception: + _set_alarm(record, alarm.MAJOR_ALARM) diff --git a/src/fastcs/transports/epics/pva/_pv_handlers.py b/src/fastcs/transports/epics/pva/_pv_handlers.py index 5ba819f98..0d102001b 100644 --- a/src/fastcs/transports/epics/pva/_pv_handlers.py +++ b/src/fastcs/transports/epics/pva/_pv_handlers.py @@ -1,3 +1,5 @@ +from typing import Any + import numpy as np from p4p import Value from p4p.nt import NTEnum, NTNDArray, NTScalar, NTTable @@ -7,13 +9,14 @@ from p4p.server.asyncio import SharedPV from fastcs.attributes import Attribute, AttrR, AttrRW, AttrW -from fastcs.datatypes import Enum, Table +from fastcs.datatypes import DataType, Table, Waveform from fastcs.methods import CommandCallback from fastcs.tracer import Tracer from .types import ( - MAJOR_ALARM_SEVERITY, - RECORD_ALARM_STATUS, + P4PAlarmState, + Severity, + Status, cast_from_p4p_value, cast_to_p4p_value, make_p4p_type, @@ -48,13 +51,21 @@ async def put(self, pv: SharedPV, op: ServerOperation): tracer.log_event("PV put", topic=self._attr_w, pv=pv, value=cast_value) - if isinstance(self._attr_w.datatype, Enum): - pv.post(cast_to_p4p_value(self._attr_w, cast_value)) - else: - pv.post(value) + datatype = self._attr_w.datatype - await self._attr_w.put(cast_value) - op.done() + _post_with_alarm_states(pv, datatype, raw_value, p4p_alarm_states()) + + try: + await self._attr_w.put(cast_value) + except Exception as e: + error_msg = f"Exception raised during put operation: {e!r}" + op.done(error=error_msg) + alarm_states = p4p_alarm_states(Severity.MAJOR, Status.RECORD, error_msg) + # Raise alarm on failed put + _post_with_alarm_states(pv, datatype, raw_value, alarm_states) + + else: + op.done() class CommandPvHandler: @@ -62,15 +73,14 @@ def __init__(self, command: CommandCallback): self._command = command self._task_in_progress = False - async def _run_command(self) -> dict: + async def _run_command(self) -> P4PAlarmState: self._task_in_progress = True try: await self._command() except Exception as e: - alarm_states = p4p_alarm_states( - MAJOR_ALARM_SEVERITY, RECORD_ALARM_STATUS, str(e) - ) + error_msg = f"Exception raised during command put: {e!r}" + alarm_states = p4p_alarm_states(Severity.MAJOR, Status.RECORD, error_msg) else: alarm_states = p4p_alarm_states() @@ -96,13 +106,25 @@ async def put(self, pv: SharedPV, op: ServerOperation): blocking = False # Flip to true once command task starts - pv.post({"value": True, **p4p_timestamp_now(), **p4p_alarm_states()}) + pv.post( + { + "value": True, + **p4p_timestamp_now(), + **p4p_alarm_states().model_dump(), + } + ) if not blocking: op.done() alarm_states = await self._run_command() - pv.post({"value": False, **p4p_timestamp_now(), **alarm_states}) + pv.post( + {"value": False, **p4p_timestamp_now(), **alarm_states.model_dump()} + ) if blocking: - op.done() + # Check if we are in alarm + if msg := alarm_states.model_dump()["alarm"]["message"]: + op.done(error=msg) + else: + op.done() else: raise RuntimeError("Commands should only take the value `True`.") @@ -153,7 +175,7 @@ async def set_setpoint(value): def make_command_pv(command: CommandCallback) -> SharedPV: type_ = NTScalar.buildType("?", display=True, control=True) - initial = Value(type_, {"value": False, **p4p_alarm_states()}) + initial = Value(type_, {"value": False, **p4p_alarm_states().model_dump()}) def _wrap(value: dict): return Value(type_, value) @@ -165,3 +187,15 @@ def _wrap(value: dict): ) return shared_pv + + +def _post_with_alarm_states( + pv: SharedPV, dtype: DataType, value: Any, alarm_states: P4PAlarmState +): + sub_states = alarm_states.model_dump()["alarm"] + if isinstance(dtype, Table | Waveform): + # NTTable and NTNDArray don't accept 'status' + sub_states.pop("status", None) + pv.post(value=value, **sub_states) + else: + pv.post({"value": value, **alarm_states.model_dump()}) diff --git a/src/fastcs/transports/epics/pva/pvi.py b/src/fastcs/transports/epics/pva/pvi.py index a68b00fde..a3ac67884 100644 --- a/src/fastcs/transports/epics/pva/pvi.py +++ b/src/fastcs/transports/epics/pva/pvi.py @@ -41,7 +41,7 @@ def _make_p4p_value( return Value( p4p_type, { - **p4p_alarm_states(), + **p4p_alarm_states().model_dump(), **p4p_timestamp_now(), **display, "value": raw_value, @@ -61,7 +61,7 @@ def _make_p4p_raw_value(pv_prefix: str, controller_api: ControllerAPI) -> dict: # Sub-device of a ControllerVector p4p_raw_value[f"__{int(pv_leaf)}"]["d"] = pv else: - p4p_raw_value[pv_leaf]["d"] = pv + p4p_raw_value[pv_leaf.lower()]["d"] = pv for pv_leaf, attribute in controller_api.attributes.items(): # Add attribute entry pv = f"{pv_prefix}:{snake_to_pascal(pv_leaf)}" diff --git a/src/fastcs/transports/epics/pva/types.py b/src/fastcs/transports/epics/pva/types.py index 75e979996..b457f3822 100644 --- a/src/fastcs/transports/epics/pva/types.py +++ b/src/fastcs/transports/epics/pva/types.py @@ -1,10 +1,12 @@ import math import time +from enum import IntEnum import numpy as np from numpy.typing import DTypeLike from p4p import Value from p4p.nt import NTEnum, NTNDArray, NTScalar, NTTable +from pydantic import BaseModel from fastcs.attributes import Attribute, AttrR, AttrW from fastcs.datatypes import Bool, DType, Enum, Float, Int, String, Table, Waveform @@ -12,12 +14,6 @@ P4P_ALLOWED_DATATYPES = (Int, Float, String, Bool, Enum, Waveform, Table) -# https://epics-base.github.io/pvxs/nt.html#alarm-t -RECORD_ALARM_STATUS = 3 -NO_ALARM_STATUS = 0 -MAJOR_ALARM_SEVERITY = 2 -NO_ALARM_SEVERITY = 0 - # https://numpy.org/devdocs/reference/arrays.dtypes.html#arrays-dtypes # Some numpy dtypes don't match directly with the p4p ones _NUMPY_DTYPE_TO_P4P_DTYPE = { @@ -109,22 +105,71 @@ def cast_from_p4p_value(attribute: Attribute[DType_T], value: object) -> DType_T raise ValueError(f"Unsupported datatype {attribute.datatype}") +class Severity(IntEnum): + """Alarm severity for a PV, as defined by the PVXS alarm_t normative type. + + Whether MINOR or MAJOR applies is context-dependent and can vary per PV. + INVALID indicates the current value should not be treated as a meaningful + reading (e.g. of the underlying measured quantity) and typically reflects + the last known good value rather than a fresh one.""" + + NO_ALARM = 0 + MINOR = 1 + MAJOR = 2 + INVALID = 3 + + +class Status(IntEnum): + """Alarm status for a PV. + + Identifies which part of the system raised the alarm, as defined by the + PVXS alarm_t normative type. + """ + + NO_ALARM = 0 + DEVICE = 1 + DRIVER = 2 + RECORD = 3 + DATABASE = 4 + CONFIGURATION = 5 + UNDEFINED = 6 + CLIENT = 7 + + +class AlarmInfo(BaseModel): + """The alarm state of a PV, following the PVXS alarm_t normative type. + + See https://epics-base.github.io/pvxs/nt.html#alarm-t. + """ + + severity: Severity = Severity.NO_ALARM + status: Status = Status.NO_ALARM + message: str = "" + + +class P4PAlarmState(BaseModel): + """Wraps AlarmInfo as the alarm sub-structure of a p4p NT* Value.""" + + alarm: AlarmInfo + + def p4p_alarm_states( - severity: int = NO_ALARM_SEVERITY, - status: int = NO_ALARM_STATUS, + severity: Severity = Severity.NO_ALARM, + status: Status = Status.NO_ALARM, message: str = "", -) -> dict: - """Returns the p4p alarm structure for a given severity, status, and message.""" - return { - "alarm": { - "severity": severity, - "status": status, - "message": message, - }, - } +) -> P4PAlarmState: + """Returns the p4p alarm structure for a given severity, status, and message, + validated as a Pydantic model.""" + return P4PAlarmState( + alarm=AlarmInfo( + severity=severity, + status=status, + message=message, + ) + ) -def p4p_timestamp_now() -> dict: +def p4p_timestamp_now() -> dict[str, dict[str, int]]: """The p4p timestamp structure for the current time.""" now = time.time() seconds_past_epoch = int(now) @@ -157,27 +202,29 @@ def p4p_display(attribute: Attribute) -> dict: return {} -def _p4p_check_numeric_for_alarm_states(datatype: Int | Float, value: DType) -> dict: +def _p4p_check_numeric_for_alarm_states( + datatype: Int | Float, value: DType +) -> dict[str, dict[str, int | str]]: low = None if datatype.min_alarm is None else value < datatype.min_alarm # type: ignore high = None if datatype.max_alarm is None else value > datatype.max_alarm # type: ignore severity = ( - MAJOR_ALARM_SEVERITY + Severity.MAJOR if high not in (None, False) or low not in (None, False) - else NO_ALARM_SEVERITY + else Severity.NO_ALARM ) - status, message = NO_ALARM_SEVERITY, "No alarm" + status, message = Status.NO_ALARM, "No alarm" if low: status, message = ( - RECORD_ALARM_STATUS, + Status.RECORD, f"Below minimum alarm limit: {datatype.min_alarm}", ) if high: status, message = ( - RECORD_ALARM_STATUS, + Status.RECORD, f"Above maximum alarm limit: {datatype.max_alarm}", ) - return p4p_alarm_states(severity, status, message) + return p4p_alarm_states(severity, status, message).model_dump() def cast_to_p4p_value(attribute: Attribute[DType_T], value: DType_T) -> object: diff --git a/tests/example_softioc.py b/tests/example_softioc.py index 14c1a0278..3688ff4fb 100644 --- a/tests/example_softioc.py +++ b/tests/example_softioc.py @@ -1,3 +1,4 @@ +import asyncio from pathlib import Path from fastcs.attributes import AttrR, AttrRW, AttrW @@ -18,11 +19,19 @@ class ParentController(Controller): class ChildController(Controller): + fail_on_next_e: bool = True c: AttrW = AttrW(Int()) @command() async def d(self): - pass + print("D: RUNNING") + await asyncio.sleep(0) + if self.fail_on_next_e: + self.fail_on_next_e = False + raise RuntimeError("D: FAILED WITH THIS WEIRD ERROR") + else: + self.fail_on_next_e = True + print("D: FINISHED") def run(id="SOFTIOC_TEST_DEVICE"): diff --git a/tests/test_attributes.py b/tests/test_attributes.py index cdd428911..41bd8547e 100644 --- a/tests/test_attributes.py +++ b/tests/test_attributes.py @@ -458,7 +458,8 @@ async def do_sync_setpoint(setpoint: int): attr.set_on_put_callback(do_put) attr.add_sync_setpoint_callback(do_sync_setpoint) - await attr.put(5) + with pytest.raises(ValueError, match="do_put failed"): + await attr.put(5) - with pytest.raises(RuntimeError): + with pytest.raises(RuntimeError, match="Attribute already has an on put callback"): attr.set_on_put_callback(do_put) diff --git a/tests/transports/epics/ca/test_softioc_system.py b/tests/transports/epics/ca/test_softioc_system.py index 7544f5308..4ba7a05ab 100644 --- a/tests/transports/epics/ca/test_softioc_system.py +++ b/tests/transports/epics/ca/test_softioc_system.py @@ -2,6 +2,7 @@ from p4p import Value from p4p.client.thread import Context +from softioc import alarm def test_ioc(softioc_subprocess: tuple[str, Queue]): @@ -52,3 +53,11 @@ def test_ioc(softioc_subprocess: tuple[str, Queue]): ctxt.put(f"{pv_prefix}:AliasB", 20, wait=True) assert ctxt.get(f"{pv_prefix}:B") == 20 assert ctxt.get(f"{pv_prefix}:B_RBV") == ctxt.get(f"{pv_prefix}:AliasB_RBV") == 20 + + # Assert command exceptions set record alarm states + ctxt.put(f"{pv_prefix}:ChildVector:0:D", True, wait=True) + # Need to put twice due to softioc bug + # where second put will propogates first put's alarm + ctxt.put(f"{pv_prefix}:ChildVector:0:D", True, wait=True) + d_alarm = ctxt.get(f"{pv_prefix}:ChildVector:0:D.SEVR") + assert d_alarm == alarm.MAJOR_ALARM diff --git a/tests/transports/epics/pva/test_p4p.py b/tests/transports/epics/pva/test_p4p.py index 947ade79d..9f97d692f 100644 --- a/tests/transports/epics/pva/test_p4p.py +++ b/tests/transports/epics/pva/test_p4p.py @@ -1,5 +1,6 @@ import asyncio import enum +from dataclasses import dataclass from datetime import datetime from multiprocessing import Queue from unittest.mock import ANY @@ -13,9 +14,9 @@ from p4p.client.thread import Context as ThreadContext from p4p.nt import NTTable -from fastcs.attributes import AttrR, AttrRW, AttrW +from fastcs.attributes import AttributeIO, AttributeIORef, AttrR, AttrRW, AttrW from fastcs.controllers import Controller, ControllerVector -from fastcs.datatypes import Bool, Enum, Float, Int, String, Table, Waveform +from fastcs.datatypes import Bool, DType_T, Enum, Float, Int, String, Table, Waveform from fastcs.launch import FastCS from fastcs.methods import command from fastcs.transports.epics.pva.transport import EpicsPVATransport @@ -148,7 +149,9 @@ async def test_command_method(p4p_subprocess: tuple[str, Queue]): assert after_command_value["value"] is False assert after_command_value["alarm"]["severity"] == 2 assert ( - after_command_value["alarm"]["message"] == "I: FAILED WITH THIS WEIRD ERROR" + after_command_value["alarm"]["message"] + == "Exception raised during command put: " + "RuntimeError('I: FAILED WITH THIS WEIRD ERROR')" ) # Failed I process does not increment J assert j_values.empty() @@ -209,6 +212,38 @@ async def test_numeric_alarms(p4p_subprocess: tuple[str, Queue]): a_monitor.close() +def test_alarms_set_if_put_fails(): + @dataclass + class SimpleAttributeIORef(AttributeIORef): + pass + + class SimpleAttributeIO(AttributeIO[DType_T, SimpleAttributeIORef]): + async def send(self, attr: AttrW[DType_T, SimpleAttributeIORef], value): + raise ValueError("Failed") + + class SomeController(Controller): + a = AttrW(Int(), io_ref=SimpleAttributeIORef()) + + controller = SomeController(ios=[SimpleAttributeIO()]) + pv_prefix = str(uuid4()) + fastcs = make_fastcs(pv_prefix, controller) + + async def put_pvs(): + await asyncio.sleep(0) + ctxt = Context("pva") + await ctxt.put(f"{pv_prefix}:A", 1) + + serve = asyncio.ensure_future(fastcs.serve(interactive=False)) + + try: + with pytest.raises(RuntimeError, match="Exception raised during put operation"): + asyncio.get_event_loop().run_until_complete( + asyncio.wait_for(asyncio.gather(serve, put_pvs()), timeout=1) + ) + finally: + serve.cancel() + + def make_fastcs(pv_prefix: str, controller: Controller) -> FastCS: controller.set_path([pv_prefix]) return FastCS(controller, [EpicsPVATransport()]) diff --git a/uv.lock b/uv.lock index 9a6d3c727..eb1866d0d 100644 --- a/uv.lock +++ b/uv.lock @@ -698,29 +698,29 @@ wheels = [ [[package]] name = "epicscorelibs" -version = "7.0.10.99.0.0" +version = "7.0.10.99.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, { name = "setuptools" }, { name = "setuptools-dso" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/0d/721e5299603b585dd6dfb97295ceb15a48170cef8ce373faa76f69322c51/epicscorelibs-7.0.10.99.0.0.tar.gz", hash = "sha256:d21c84807bb329f4b8752d1c9638a4e60f42df2dc837b6286f0b2d5515129a3d", size = 1669631, upload-time = "2026-01-08T01:29:46.207Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/8e/3ab7107913f567947eb9c4ff1c948823b0e7069361358a7567e136e527d3/epicscorelibs-7.0.10.99.0.1.tar.gz", hash = "sha256:e8c8d9730faef7df55f4d329c359a0e4c044f03a524dd2e4587e90132873edfd", size = 1669710, upload-time = "2026-02-11T01:39:48.704Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/55/972ecdd386d28db0a5b18f0a84d5210401a623abec0cfe25dff2d3202c0b/epicscorelibs-7.0.10.99.0.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:1e4a64189b04649802a325b65e70aa566e62fc2ac827652b2f0fcd86ad784c4a", size = 7636875, upload-time = "2026-01-08T01:29:14.189Z" }, - { url = "https://files.pythonhosted.org/packages/a2/94/f79d480f538086a23871a88574baee4934f88c46a3002adb9895fe1f7fca/epicscorelibs-7.0.10.99.0.0-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:5a78ce633f6f0bae6675c9e128145480a1e428cc47495e2580eb05db2f6b37ba", size = 5421557, upload-time = "2026-01-08T01:29:15.995Z" }, - { url = "https://files.pythonhosted.org/packages/92/04/f8899630e6beb535596f7408016df4a16ef57bf2221214d307373771bd6f/epicscorelibs-7.0.10.99.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:227b16e9f4ba64350943f96ad866f69e104de2ea8f8bf31e8de6b4c3b99b20b5", size = 2533327, upload-time = "2026-01-08T01:29:17.316Z" }, - { url = "https://files.pythonhosted.org/packages/6a/66/a559c144bd0993c4e0ae5b91e9cee8dd2edb171242c5e4937cef1404bf0e/epicscorelibs-7.0.10.99.0.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:c20fbcec49effb9658e917317c091c099d00204dc0084881cb61c4a18f856cfb", size = 7636035, upload-time = "2026-01-08T01:29:19.235Z" }, - { url = "https://files.pythonhosted.org/packages/6e/b4/03e3d2a6b8c9ed552674e7b47715861719f02e08ca6754adc863e6644112/epicscorelibs-7.0.10.99.0.0-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:b62015fb52b315fe5b612d14f570fa2da7de5e6adf80a108deccbdcff6651f4b", size = 5423699, upload-time = "2026-01-08T01:29:21.785Z" }, - { url = "https://files.pythonhosted.org/packages/dd/73/62942246aad56d98bff18c2603a64be3f800763d01e69fd89f9fcbf5c0a0/epicscorelibs-7.0.10.99.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:1c04ca38cacecbac749a7d6520b8d0b038ac7257e6c161780ae99f9168dd1d9c", size = 2533275, upload-time = "2026-01-08T01:29:23.514Z" }, - { url = "https://files.pythonhosted.org/packages/fe/69/d9225563e9aac120db7461aa81c558f245db6a1e5a08e82dd538d6ca5d8d/epicscorelibs-7.0.10.99.0.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:c8620955b611983181e8b283f06ce2690a8d2a7955d9bcd2b48abb623bdbc736", size = 7635896, upload-time = "2026-01-08T01:29:25.367Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5e/8dcec23dd61a60f12d6c90b580ba32d356261790eb7aac59a03190a4f7d0/epicscorelibs-7.0.10.99.0.0-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:b25b511955020138f13bb7231cf05840034fa092e47e8950e870519ac60e58ed", size = 5423685, upload-time = "2026-01-08T01:29:27.311Z" }, - { url = "https://files.pythonhosted.org/packages/82/d5/a300e6605f54a7daae3828aeee14b8c3b6f64c5fde931a058a8fd8653b1b/epicscorelibs-7.0.10.99.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:eb4e533efa6edfced2bf7d0f243087df123b5d9556a30074ba20f8ad75c8e195", size = 2533255, upload-time = "2026-01-08T01:29:28.727Z" }, - { url = "https://files.pythonhosted.org/packages/a4/20/0034825cf77ee96ec1de6c5d3272bf6caa791e171dbdf4f7ed7067695ab6/epicscorelibs-7.0.10.99.0.0-cp313-cp313t-manylinux2014_x86_64.whl", hash = "sha256:0700a6025166f4ae2c1c5dde50795c063f9616925a7df0b5ef736c27b3965926", size = 5423710, upload-time = "2026-01-08T01:29:30.571Z" }, - { url = "https://files.pythonhosted.org/packages/3a/a5/7b6112e32379fd004357fdb5a3b004fddba41d38fce1ad1044aff9444853/epicscorelibs-7.0.10.99.0.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:6599d4625d6e0cb7bfbba4d12952c5ba2a5f20ef64e5c936da4169d0ac172013", size = 7636181, upload-time = "2026-01-08T01:29:31.929Z" }, - { url = "https://files.pythonhosted.org/packages/3b/a1/a76f33feb57c85ebe326cb42867b8387f9ce2383e589cf88516632f89411/epicscorelibs-7.0.10.99.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d7ba960dcf02628398dc29464dafbf70a3d689cfdf0418f7597c3e13a45590da", size = 5358457, upload-time = "2026-01-08T01:29:33.268Z" }, - { url = "https://files.pythonhosted.org/packages/06/9b/8e0069ceb5c8cd1286c0fe3a3fedafdd29c5825b96c3429d628f1e1c36b1/epicscorelibs-7.0.10.99.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:7b4b3da922b69e637126b6eb5e65d77a594d58021d74010fdcef376245be40c0", size = 2587368, upload-time = "2026-01-08T01:29:35.016Z" }, - { url = "https://files.pythonhosted.org/packages/04/e8/464a91826103cddeb772d60250ce84ea3fdd84ff90bdc387c9483a777ad7/epicscorelibs-7.0.10.99.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:f0569ab084e149385f6fcfa3c441f9f43099357ad54ae21f9496220861b02d38", size = 5358471, upload-time = "2026-01-08T01:29:36.529Z" }, + { url = "https://files.pythonhosted.org/packages/14/9b/69a26ca4c0f6afda101f790339f3c05deef379f3ee3c12bb7be730542b05/epicscorelibs-7.0.10.99.0.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:98be3cea1239bc3076cc8e86759b4fa00903123e42c96664215af1063d177f55", size = 7636846, upload-time = "2026-02-11T01:39:20.647Z" }, + { url = "https://files.pythonhosted.org/packages/12/29/ce9c8e81a6b9be7722ac00cb592ce6b652405eecb178bf410810732957ec/epicscorelibs-7.0.10.99.0.1-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:91f112790a31c296bdb2c0f9b661e993c3ded9df9f4731a92887c5abfbaf568a", size = 5421614, upload-time = "2026-02-11T01:39:22.053Z" }, + { url = "https://files.pythonhosted.org/packages/46/b5/dd2b4277ad1eaf49593e1fda042446423326075e991047f50269541585bc/epicscorelibs-7.0.10.99.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ca9784fd25d362d9248174c55df3f3cd902b7f51fb8cd165b4f1748c713293e", size = 2533399, upload-time = "2026-02-11T01:39:23.25Z" }, + { url = "https://files.pythonhosted.org/packages/10/fc/27b418beb7e7598783fe9a9eea830144b9e1a130c338d7da3ecd354447ad/epicscorelibs-7.0.10.99.0.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:cbc6457df698f916bec7bd9eb242b280e3e6e4a6a2379da86974a571566d618e", size = 7636199, upload-time = "2026-02-11T01:39:24.923Z" }, + { url = "https://files.pythonhosted.org/packages/4f/7b/55f41ec7746713db21aff5e794fc98b5a2e4c40fc72d69f6a1cdb04df360/epicscorelibs-7.0.10.99.0.1-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:b7b9cb9dc1b533f6bb569dffe246256ba4d553a5843ddf494645c9b33a38ba2a", size = 5423739, upload-time = "2026-02-11T01:39:27.582Z" }, + { url = "https://files.pythonhosted.org/packages/09/28/fe6e34a35bb1f9c1873e56e7561cfb2646a47299e7fce5f757d70fafd2cc/epicscorelibs-7.0.10.99.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:28cc23c97c9ddbe7be67c036b37ecf945923351dec5894c5894afb7883ac099f", size = 2533296, upload-time = "2026-02-11T01:39:28.795Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0f/0643fffd4852c4af6d40415c0689e45947a5fc51e6093a15543ca375ae43/epicscorelibs-7.0.10.99.0.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:fa350350587b87080a8200554fae2fdbffe92fa0c9fb9e9d68c7d69577070ce8", size = 7636035, upload-time = "2026-02-11T01:39:30.03Z" }, + { url = "https://files.pythonhosted.org/packages/7c/aa/3b0faa7eea6a2957af7ee116f08b5edd99eff7e7461e22ac613b0a8d38ea/epicscorelibs-7.0.10.99.0.1-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:8ac0a67067d06c42d4ac83b685f8020d4e993858bc4e36d58631983b1117aa39", size = 5423737, upload-time = "2026-02-11T01:39:31.355Z" }, + { url = "https://files.pythonhosted.org/packages/45/a0/10099c41bdae2312b45816802a496bd1dee301e6f00108eec29dd498f6c9/epicscorelibs-7.0.10.99.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:5d22a8fbb26414e613fba77a9422a289fc30b05b32548c79644ef39d04f76743", size = 2533281, upload-time = "2026-02-11T01:39:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/f8/01/2182e903255db558c77b2a455de5381235259cf03737ea99b089adf4d905/epicscorelibs-7.0.10.99.0.1-cp313-cp313t-manylinux2014_x86_64.whl", hash = "sha256:4b2f8228844f73b06412075e50c6787e5d19af6d3ed230c7804b3765f1aa7274", size = 5423756, upload-time = "2026-02-11T01:39:35.109Z" }, + { url = "https://files.pythonhosted.org/packages/9a/94/245e2f0c3155b00a49a9376bcedb9d4b72cc9c36de5d73b4b55d0b69b293/epicscorelibs-7.0.10.99.0.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d747ad55055a5031630063f24695223f9d983f3389fd0a2133640c6f6f92043", size = 7636529, upload-time = "2026-02-11T01:39:36.388Z" }, + { url = "https://files.pythonhosted.org/packages/55/57/42c5bb50507c528d9af10879067920d27c5a6b33fbef16c2e1b55de77ccc/epicscorelibs-7.0.10.99.0.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:7de58f4e8a7dd8edf7b808ffc99391e95ca3516c4f2780258c31f4f844d63239", size = 5358518, upload-time = "2026-02-11T01:39:37.699Z" }, + { url = "https://files.pythonhosted.org/packages/13/83/ba7b1534760786b45bdbe590bf9b972ba594582c80ca4ac473371321a6c4/epicscorelibs-7.0.10.99.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e2f294e60a0b3a50af5c26cda8c62b1ba926742f3a7d562e9ae65432dd16c18", size = 2587325, upload-time = "2026-02-11T01:39:39.181Z" }, + { url = "https://files.pythonhosted.org/packages/e6/94/5c7b17b3ff1962f9fe04b07d45008bccb3a6236887bb426babaa5690d372/epicscorelibs-7.0.10.99.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:045751cdbb9bda21e830bd92753c259d10c8582c2e2bbd3dfa768158a57f541b", size = 5358535, upload-time = "2026-02-11T01:39:40.386Z" }, ] [[package]] @@ -1008,7 +1008,7 @@ requires-dist = [ { name = "pygelf" }, { name = "pytango", marker = "extra == 'tango'" }, { name = "ruamel-yaml" }, - { name = "softioc", marker = "extra == 'epicsca'", specifier = ">=4.5.0" }, + { name = "softioc", marker = "extra == 'epicsca'", git = "https://github.com/DiamondLightSource/pythonSoftIOC.git?rev=master" }, { name = "stdio-socket" }, { name = "strawberry-graphql", marker = "extra == 'graphql'" }, { name = "tickit", marker = "extra == 'demo'", specifier = "~=0.4.3" }, @@ -3007,8 +3007,8 @@ wheels = [ [[package]] name = "softioc" -version = "4.7.0" -source = { registry = "https://pypi.org/simple" } +version = "4.7.0+10.g1711080" +source = { git = "https://github.com/DiamondLightSource/pythonSoftIOC.git?rev=master#1711080f541b36175bf59de05bb46794008390cb" } dependencies = [ { name = "epicscorelibs" }, { name = "epicsdbbuilder" }, @@ -3016,18 +3016,6 @@ dependencies = [ { name = "pvxslibs" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/64/fd/7b42c8c57703abbae0e78c351291eee1414bdc8219754ac8fcb2160605c5/softioc-4.7.0.tar.gz", hash = "sha256:8514c6f6b9df1191462171df6a2b9d9b33e6492fe252d3c5839d2f132d307add", size = 92691, upload-time = "2026-01-14T14:07:48.473Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/39/34/2e64ab5adc6708cd4344a757746d842d86b18b818d986e9b341917a7bfb3/softioc-4.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8a7c66f3ed372659ede74ff145b6c5ff041c33ff7921d744c01e6b4f1a1df5e", size = 118815, upload-time = "2026-01-14T14:07:31.079Z" }, - { url = "https://files.pythonhosted.org/packages/36/5f/e16232e8bcf05ab633c39bb7ae2f1ed334b686383db41d20e2a59500ceeb/softioc-4.7.0-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:1bcf79de8cbe49599cecbafa01c145058e60a22c055408609da95afd3fcbdc4b", size = 124586, upload-time = "2026-01-14T14:07:32.462Z" }, - { url = "https://files.pythonhosted.org/packages/d9/bc/622aa9138057c8c8249a1ad94bd2a12d69e814f327b988b8545891d93a15/softioc-4.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:4413a2ed7ae4f0bddc9179a9ec1afa2b514343de1aa72bb5a327fdfe1a2f1729", size = 118430, upload-time = "2026-01-14T14:07:33.682Z" }, - { url = "https://files.pythonhosted.org/packages/57/cc/5b4a59eebfb419ab2e9ffa58b4c09ce89af896b13c60516b5c5e42e86c89/softioc-4.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8e101f4f3ad35c32fecbca422dcf6496ec3c98545965fedc5a9dc48ba0dbe37", size = 118811, upload-time = "2026-01-14T14:07:34.519Z" }, - { url = "https://files.pythonhosted.org/packages/23/62/d59270acbec972aea0c36d65cc13673e786c12664070cf437869678655e9/softioc-4.7.0-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:4fe5cb8fa3c156b81940b7e9a456fe90a55ec1194eb75d4c286af405ff27ab22", size = 124622, upload-time = "2026-01-14T14:07:35.429Z" }, - { url = "https://files.pythonhosted.org/packages/03/81/f976b094616fb605dad6d21a0b4b78278e3585439ec5cb2ae4da1ff52441/softioc-4.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:3b2ac39cb9223fd936a9ffe5676dce6d7c499a5d1f5919380085829c1c38bf07", size = 118457, upload-time = "2026-01-14T14:07:36.822Z" }, - { url = "https://files.pythonhosted.org/packages/58/fe/8c9bf481416f8f9a6bd6e47b3ee7fb5282c320731e66ab47ccbbca9164b2/softioc-4.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eda2a04105c0c2f397068ec074e43db2264dd330baa2d7ac2a94702f2fa4cab3", size = 118819, upload-time = "2026-01-14T14:07:37.727Z" }, - { url = "https://files.pythonhosted.org/packages/47/90/d96f39b25f4cca886ac3f36a1bbfb1b04daef9512ec295d6319bcb7e2707/softioc-4.7.0-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:3a662a934af3c46eb7f5390539aa210a72fef5b48dd92167c4dd248bdc234597", size = 124619, upload-time = "2026-01-14T14:07:39.761Z" }, - { url = "https://files.pythonhosted.org/packages/f8/b2/bf80cc846aed9aa2d87c29e613e9d55932c9a180962da9c5de97d6da8b73/softioc-4.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:f93647ab279856e9653c545a626ed40aea71d3432391c3df721655d4888c656b", size = 118454, upload-time = "2026-01-14T14:07:40.637Z" }, -] [[package]] name = "soupsieve"