from __future__ import annotations
import copy
import inspect
import re
import time
import warnings
from abc import ABC, abstractmethod
from functools import reduce
from itertools import chain
from typing import Any, List, Union, cast
import numpy as np
from bluesky.protocols import Reading
from event_model import DataKey
from frappy.client import CacheItem
from frappy.datatypes import (
ArrayOf,
BLOBType,
BoolType,
CommandType,
DataType,
EnumType,
FloatRange,
IntRange,
ScaledInteger,
StringType,
StructOf,
TupleOf,
)
from ophyd_async.core._utils import StrictEnum
from typing_extensions import NotRequired
SCALAR_DATATYPES = (
IntRange,
BoolType,
FloatRange,
ScaledInteger,
StringType,
EnumType,
)
[docs]
class SECoPDataKey(DataKey):
"""
A DataKey that is used to describe the SECoP Datatype.
"""
dtype_str: NotRequired[str] # type: ignore
"""
The array-protocol typestring of the data-type object.
"""
dtype_descr: NotRequired[list] # type: ignore
"""
String representation of the numpy structured array dtype.
"""
SECOP_datainfo: str
"""
String representation of the original secop datatype.
"""
SECoP_dtype: NotRequired[str] # type: ignore
"""
The SECoP datatype of the data.
"""
[docs]
class NestedRaggedArray(Exception):
"""The Datatype contains nested ragged arrays"""
[docs]
def deep_get(dictionary, keys, default=None) -> dict:
def get_val(obj, key, default):
if isinstance(obj, dict):
return obj.get(key, default)
if isinstance(obj, list):
return obj[key]
if isinstance(obj, tuple):
return obj[key]
return default
return reduce(lambda d, key: get_val(d, key, default), keys, dictionary)
[docs]
class Path:
def __init__(self, parameter_name: str, module_name: str) -> None:
self._accessible_name = parameter_name
self._module_name = module_name
self._last_named_param: int | None = None
self._dev_path: List[Union[str, int]] = []
# Path is extended
[docs]
def append(self, elem: Union[str, int]) -> Path:
new_path = copy.deepcopy(self)
if isinstance(elem, str):
new_path._last_named_param = len(new_path._dev_path)
new_path._dev_path.append(elem)
return new_path
[docs]
def get_param_path(self):
return {"module": self._module_name, "parameter": self._accessible_name}
[docs]
def get_path_tuple(self):
return (self._module_name, self._accessible_name)
# TODO typing of return value
[docs]
def get_memberinfo_path(self):
# inserting K after every Nth number
# using itertool.chain()
# insert element
k = "members"
# insert after every other element
n = 1
# using itertool.chain()
# inserting K after every Nth number
return list(
chain(
*[
(
[k] + self._dev_path[i : i + n] # type: ignore
if len(self._dev_path[i : i + n]) == n
else self._dev_path[i : i + n]
)
for i in range(0, len(self._dev_path), n)
]
)
)
[docs]
def get_signal_name(self):
# top level: signal name == Parameter name
if self._dev_path == []:
return self._accessible_name
sig_postfix = self._dev_path[self._last_named_param :]
if self._last_named_param is None:
sig_postfix = [self._accessible_name] + sig_postfix # type: ignore
delim = "-"
return delim.join(map(str, sig_postfix))
[docs]
def get_param_desc_path(self):
return [self._module_name, "parameters", self._accessible_name]
[docs]
def get_cmd_desc_path(self):
return [self._module_name, "commands", self._accessible_name]
[docs]
def get_leaf(self):
if self._dev_path == []:
return None
return self._dev_path[-1]
[docs]
def insert_val(self, dic: dict, new_val):
if self._dev_path == []:
return dic
d = dic
for key in self._dev_path[:-1]:
if key in d:
d = d[key]
else:
# wrong path
raise Exception(
"path is incorrect " + str(key) + " is not in dict: " + str(dic)
)
# insert new value
if self._dev_path[-1] in d:
d[self._dev_path[-1]] = new_val
else:
# wrong path
raise Exception(
"path is incorrect " + str(key) + " is not in dict: " + str(dic)
)
return dic
[docs]
class DtypeNP(ABC):
secop_dtype: DataType
name: str | None
array_element: bool = False
max_depth: int = 0
[docs]
@abstractmethod
def make_numpy_dtype(self) -> tuple:
"""Create Numpy Compatible structured Datatype"""
[docs]
@abstractmethod
def make_concrete_numpy_dtype(self, value) -> tuple:
"""Create Numpy Compatible structured Datatype from a concrete data value"""
[docs]
@abstractmethod
def make_numpy_compatible_list(self, value) -> Any:
"""make a make a list that is ready for numpy array import"""
[docs]
@abstractmethod
def make_secop_compatible_object(self, value) -> Any:
"""make a make an SECoP Compatible Object"""
[docs]
def dt_factory(
secop_dt: DataType, name: str = "", array_element: bool = False
) -> DtypeNP:
dt_class = secop_dt.__class__
dt_converters = {
StructOf: StructNP,
TupleOf: TupleNP,
ArrayOf: ArrayNP,
IntRange: IntNP,
FloatRange: FloatNP,
ScaledInteger: ScaledIntNP,
BLOBType: BLOBNP,
BoolType: BoolNP,
EnumType: EnumNP,
StringType: StringNP,
}
return dt_converters[dt_class](secop_dt, name, array_element) # type: ignore
STR_LEN_DEFAULT = 100
[docs]
class BLOBNP(DtypeNP):
def __init__(
self, blob_dt: BLOBType, name: str = "", array_element: bool = False
) -> None:
self.name: str = name
self.secop_dtype: BLOBType = blob_dt
self.array_element = array_element
[docs]
def make_numpy_dtype(self) -> tuple:
return (self.name, "U" + str(self.secop_dtype.maxbytes))
[docs]
def make_concrete_numpy_dtype(self, value) -> tuple:
return (self.name, "U" + str(self.secop_dtype.maxbytes))
[docs]
def make_numpy_compatible_list(self, value: str):
return value
[docs]
def make_secop_compatible_object(self, value) -> Any:
return value
[docs]
class BoolNP(DtypeNP):
def __init__(
self, bool_dt: BoolType, name: str = "", array_element: bool = False
) -> None:
self.name: str = name
self.secop_dtype: BoolType = bool_dt
self.array_element = array_element
[docs]
def make_numpy_dtype(self) -> tuple:
return (self.name, "<b1")
[docs]
def make_concrete_numpy_dtype(self, value) -> tuple:
return self.make_numpy_dtype()
[docs]
def make_numpy_compatible_list(self, value: bool):
return value
[docs]
def make_secop_compatible_object(self, value) -> Any:
return bool(value)
[docs]
class EnumNP(DtypeNP):
def __init__(
self, enum_dt: EnumType, name: str = "", array_element: bool = False
) -> None:
self.name: str = name
self.secop_dtype: EnumType = enum_dt
self.array_element = array_element
[docs]
def make_numpy_dtype(self) -> tuple:
return (self.name, "<i8")
[docs]
def make_concrete_numpy_dtype(self, value) -> tuple:
return self.make_numpy_dtype()
[docs]
def make_numpy_compatible_list(self, value: int):
return value
[docs]
def make_secop_compatible_object(self, value) -> Any:
return int(value)
[docs]
class FloatNP(DtypeNP):
def __init__(
self, float_dt: FloatRange, name: str = "", array_element: bool = False
) -> None:
self.name: str = name
self.secop_dtype: FloatRange = float_dt
self.array_element = array_element
[docs]
def make_numpy_dtype(self) -> tuple:
return (self.name, "<f8")
[docs]
def make_concrete_numpy_dtype(self, value) -> tuple:
return self.make_numpy_dtype()
[docs]
def make_numpy_compatible_list(self, value: float):
return value
[docs]
def make_secop_compatible_object(self, value) -> Any:
return float(value)
[docs]
class IntNP(DtypeNP):
def __init__(
self, int_dt: IntRange, name: str = "", array_element: bool = False
) -> None:
self.name: str = name
self.secop_dtype: IntRange = int_dt
self.array_element = array_element
[docs]
def make_numpy_dtype(self) -> tuple:
return (self.name, "<i8")
[docs]
def make_concrete_numpy_dtype(self, value) -> tuple:
return self.make_numpy_dtype()
[docs]
def make_numpy_compatible_list(self, value: int):
return value
[docs]
def make_secop_compatible_object(self, value) -> Any:
return int(value)
[docs]
class ScaledIntNP(DtypeNP):
def __init__(
self, scaled_int_dt: ScaledInteger, name: str = "", array_element: bool = False
) -> None:
self.name: str = name
self.secop_dtype: ScaledInteger = scaled_int_dt
self.array_element = array_element
[docs]
def make_numpy_dtype(self) -> tuple:
return (self.name, "<i8")
[docs]
def make_concrete_numpy_dtype(self, value) -> tuple:
return self.make_numpy_dtype()
[docs]
def make_numpy_compatible_list(self, value: int):
return value
[docs]
def make_secop_compatible_object(self, value) -> Any:
return int(value)
[docs]
class StringNP(DtypeNP):
def __init__(
self, string_dt: StringType, name: str = "", array_element: bool = False
) -> None:
self.name: str = name
self.secop_dtype: StringType = string_dt
self.array_element = array_element
if string_dt.maxchars == 1 << 64:
# warnings.warn(
# "maxchars was not set, default max char lenght is set to: "
# + str(STR_LEN_DEFAULT)
# )
self.strlen = STR_LEN_DEFAULT
else:
self.strlen = string_dt.maxchars
[docs]
def make_numpy_dtype(self) -> tuple:
return (self.name, "<U" + str(self.strlen))
[docs]
def make_concrete_numpy_dtype(self, value) -> tuple:
return (self.name, "<U" + str(self.strlen))
[docs]
def make_numpy_compatible_list(self, value: str):
return value
[docs]
def make_secop_compatible_object(self, value) -> Any:
return str(value)
[docs]
class StructNP(DtypeNP):
def __init__(
self, struct_dt: StructOf, name: str = "", array_element: bool = False
) -> None:
self.name: str = name
self.secop_dtype: StructOf = struct_dt
self.array_element = array_element
self.members: dict[str, DtypeNP] = {
name: dt_factory(member, name, self.array_element)
for (name, member) in struct_dt.members.items()
}
max_depth = [member.max_depth for member in self.members.values()]
self.max_depth = 1 + max(max_depth)
[docs]
def make_numpy_dtype(self) -> tuple:
dt_list = []
for member in self.members.values():
dt_list.append(member.make_numpy_dtype())
return (self.name, dt_list)
[docs]
def make_concrete_numpy_dtype(self, value) -> tuple:
dt_list = []
for key, member in self.members.items():
member_val = value[key]
dt_list.append(member.make_concrete_numpy_dtype(member_val))
return (self.name, dt_list)
[docs]
def make_numpy_compatible_list(self, value: dict):
return tuple(
[
self.members[name].make_numpy_compatible_list(value[name])
for name in self.members.keys()
]
)
[docs]
def make_secop_compatible_object(self, value) -> Any:
return {
member: np_dtype.make_secop_compatible_object(value=value[member])
for (member, np_dtype) in self.members.items()
}
[docs]
class TupleNP(DtypeNP):
def __init__(
self, tuple_dt: TupleOf, name: str = "", array_element: bool = False
) -> None:
self.name: str = name
self.secop_dtype = tuple_dt
self.array_element = array_element
self.members: list[DtypeNP] = [
dt_factory(
secop_dt=member, name="f" + str(idx), array_element=self.array_element
)
for idx, member in enumerate(tuple_dt.members)
]
max_depth = [member.max_depth for member in self.members]
self.max_depth = 1 + max(max_depth)
[docs]
def make_numpy_dtype(self) -> tuple:
dt_list = []
for member in self.members:
dt_list.append(member.make_numpy_dtype())
return (self.name, dt_list)
[docs]
def make_concrete_numpy_dtype(self, value) -> tuple:
dt_list = []
for index, member in enumerate(self.members):
member_val = value[index]
dt_list.append(member.make_concrete_numpy_dtype(member_val))
return (self.name, dt_list)
[docs]
def make_numpy_compatible_list(self, value: tuple):
return tuple(
[
self.members[idx].make_numpy_compatible_list(value[idx])
for idx, member in enumerate(self.members)
]
)
[docs]
def make_secop_compatible_object(self, value) -> Any:
return tuple(
[
np_dtype.make_secop_compatible_object(value=value["f" + str(idx)])
for idx, np_dtype in enumerate(self.members)
]
)
[docs]
class ArrayNP(DtypeNP):
def __init__(
self, array_dt: ArrayOf, name: str = "", array_element: bool = False
) -> None:
self.name: str = name
self.secop_dtype = array_dt
self.maxlen = array_dt.maxlen
self.minlen = array_dt.minlen
self.array_element = array_element
self.ragged: bool = self.minlen != self.maxlen
self.shape = [self.maxlen]
self.members: DtypeNP = dt_factory(array_dt.members, array_element=True)
self.root_type: DtypeNP
self.max_depth = self.members.max_depth
if isinstance(self.members, ArrayNP):
self.shape.extend(self.members.shape)
self.root_type = self.members.root_type
self.members.shape = []
if self.members.ragged:
pass
# raise NestedRaggedArray(
# "ragged arrays with more than a single dimension are not supported"
# )int
else:
self.root_type = self.members
if self.array_element and self.ragged:
warnings.warn(
"ragged arrays inside of arrays of copmposite datatypes (struct/tuple)"
"are not supported"
)
[docs]
def get_root_np_str(self) -> str:
dtype_list = self.root_type.make_numpy_dtype()
return dtype_list[1]
[docs]
def make_numpy_dtype(self) -> tuple:
if self.shape == []:
return self.members.make_numpy_dtype()
else:
return (self.name, list(self.members.make_numpy_dtype()).pop(), self.shape)
[docs]
def make_concrete_numpy_dtype(self, value) -> tuple:
if self.ragged is False:
return (
self.name,
list(self.members.make_concrete_numpy_dtype(value)).pop(),
self.shape,
)
else:
if value == []:
member_np = self.members.make_concrete_numpy_dtype(None)
val_shape = [0]
elif value is None:
member_np = self.members.make_concrete_numpy_dtype(None)
val_shape = []
else:
member_np = self.members.make_concrete_numpy_dtype(value[0])
val_shape = [len(value)]
if isinstance(self.members, ArrayNP):
val_shape = val_shape + member_np[2]
return (self.name, member_np[1], val_shape)
[docs]
def make_numpy_compatible_list(self, value: list):
return [self.members.make_numpy_compatible_list(elem) for elem in value]
[docs]
def make_secop_compatible_object(self, value: np.ndarray) -> Any:
return [
self.members.make_secop_compatible_object(value[idx])
for idx in range(0, self.maxlen)
]
[docs]
class SECoPdtype:
def __init__(self, datatype: DataType) -> None:
self.raw_dtype: DataType = datatype
# Describe Fields ------------------------
self.dtype: str
# The array-protocol typestring of the data-type object.
self.dtype_str: str
# String representation of the numpy structured array dtype
self.dtype_descr: list
# string representation of the original secop datatype
self.secop_dtype_str = str(datatype.export_datatype())
# Shape of Data
self.shape = []
self.np_datatype: Any
# Describe Fields ------------------------
self.numpy_dtype: np.dtype
self.dtype_tree: DtypeNP
self._is_composite: bool = False
self._is_array: bool = False
self._is_enum: bool = False
self.dtype_tree = dt_factory(datatype)
self.max_depth: int = self.dtype_tree.max_depth
if isinstance(self.dtype_tree, EnumNP):
self._is_enum = True
if isinstance(self.dtype_tree, ArrayNP):
self.shape = self.dtype_tree.shape
self._is_array = True
self._is_composite = (
True
if isinstance(self.dtype_tree.root_type, (StructNP, TupleNP))
else False
)
self._is_enum = self._is_enum or isinstance(
self.dtype_tree.root_type, EnumNP
)
if isinstance(self.dtype_tree, (TupleNP, StructNP)):
self._is_composite = True
# Composite Datatypes & Arrays of COmposite Datatypes
if self._is_composite:
dt = self.dtype_tree.make_numpy_dtype()
# Top level elements are not named and shape is
# already covered by the shape var
dt = dt[1]
self.numpy_dtype = np.dtype(dt)
# all composite Dtypes are transported as numpy arrays
self.dtype = "array"
self.dtype_str = self.numpy_dtype.str
self.dtype_descr = self.numpy_dtype.descr
self.np_datatype = np.ndarray
# Scalar atomic Datatypes and arrays of atomic dataypes
else:
if self._is_array:
# root secop datatype that is contained in the array
self.dtype = "array"
self.np_datatype = np.ndarray
# Primitive datatypes
else:
self.np_datatype = SECOP2DTYPE[datatype.__class__][0]
self.dtype = SECOP2DTYPE[datatype.__class__][1]
[docs]
def get_datakey(self) -> dict:
describe_dict: dict = {}
# Composite Datatypes & Arrays of COmposite Datatypes
if self._is_composite:
describe_dict["dtype_str"] = self.dtype_str
# describe_dict["dtype_numpy"] = self.dtype_descr
describe_dict["dtype_descr"] = self.dtype_descr
if isinstance(self.dtype_tree, ArrayNP):
describe_dict["dtype_numpy"] = self.dtype_tree.get_root_np_str()
describe_dict["dtype"] = self.dtype
describe_dict["shape"] = self.shape
describe_dict["SECOP_datainfo"] = self.secop_dtype_str
return describe_dict
def _secop2numpy_array(self, value) -> np.ndarray:
np_list = self.dtype_tree.make_numpy_compatible_list(value)
return np.array(np_list, dtype=self.numpy_dtype)
[docs]
def secop2val(self, reading_val) -> Any:
if self._is_composite:
return self._secop2numpy_array(reading_val)
elif self._is_enum:
if isinstance(self.dtype_tree, EnumNP):
exp_val = self.dtype_tree.secop_dtype(reading_val).name
return exp_val
elif isinstance(self.dtype_tree, ArrayNP) and isinstance(
self.dtype_tree.root_type, EnumNP
):
def map_enum(val):
return list(
map(
lambda x: map_enum(x) if isinstance(x, tuple) else x.name,
val,
)
)
exp_val = map_enum(reading_val)
return exp_val
else:
raise Exception("enum type is not correctly identified in dtype tree")
else:
return reading_val
[docs]
def val2secop(self, input_val) -> Any:
# TODO check input_Val for conformity with datatype
# TODO check if it is already in SECoP Format
if self._is_composite and isinstance(input_val, np.ndarray):
return self.dtype_tree.make_secop_compatible_object(input_val)
else:
return self.raw_dtype.validate(input_val)
[docs]
def update_dtype(self, input_val):
if self._is_composite:
# Composite Datatypes & Arrays of Composite Datatypes
dt = self.dtype_tree.make_concrete_numpy_dtype(input_val)
# Top level elements are not named and shape is
# already covered by the shape var
inner_dt = dt[1]
if isinstance(self.raw_dtype, ArrayOf):
self.shape = dt[2]
self.numpy_dtype = np.dtype(inner_dt)
self.dtype_str = self.numpy_dtype.str
self.dtype_descr = self.numpy_dtype.descr
return
if isinstance(self.raw_dtype, ArrayOf):
dt = self.dtype_tree.make_concrete_numpy_dtype(input_val)
self.shape = dt[2]
[docs]
def secop_enum_name_to_python(member_name: str) -> str:
"""Convert SECoP enum member name to Python identifier.
Examples:
'Low Energy' -> 'LOW_ENERGY'
'high-power' -> 'HIGH_POWER'
'Mode 1' -> 'MODE_1'
:param member_name: Original SECoP enum member name
:return: Python-compatible identifier in UPPER_CASE
"""
# Replace spaces and hyphens with underscores, remove other special chars
cleaned = re.sub(r"[\s-]+", "_", member_name)
cleaned = re.sub(r"[^a-zA-Z0-9_]", "", cleaned)
# Convert to uppercase
cleaned = cleaned.upper()
# Ensure it doesn't start with a digit
if cleaned and cleaned[0].isdigit():
cleaned = "_" + cleaned
return cleaned
# Maps a SECoP command argument/result datatype to the plain python type used
# for both the runtime bluesky-plan-method signature and the generated
# Command[[ArgT], ResT] annotation (as opposed to SECoPdtype.np_datatype, which
# is the numpy-facing type used for Parameter/Property SignalR annotations).
COMMAND_DTYPE_MAPPING: dict[type[DataType], type] = {
StructOf: dict[str, Any],
ArrayOf: list[Any],
TupleOf: tuple[Any],
BLOBType: str,
BoolType: bool,
EnumType: StrictEnum,
FloatRange: float,
IntRange: int,
ScaledInteger: int,
StringType: str,
}
[docs]
def command_dtype_to_python_type(datatype: DataType) -> type:
"""Map a SECoP command argument/result datatype to a plain python type."""
return COMMAND_DTYPE_MAPPING[datatype.__class__]
def _dynamic_enum_class(enum_dt: EnumType, context_name: str) -> type[StrictEnum]:
"""Dynamically build a StrictEnum subclass with the real SECoP member
names/values, for a command signature annotation built at pure-runtime
introspection (no codegen involved, so no generated source file to name
a concrete class in) -- gives `build_command_signature()` a real,
member-carrying enum instead of the generic `StrictEnum` base class,
using Python's functional Enum API rather than generated source text.
"""
members = enum_dt.export_datatype().get("members", {})
class_name = f"{context_name.capitalize()}Enum"
# mypy doesn't recognize the functional Enum API on a custom StrEnum
# subclass with its own metaclass; this is exercised at runtime in
# tests/test_classgen.py.
enum_cls = StrictEnum( # type: ignore[call-arg]
class_name, {secop_enum_name_to_python(name): name for name in members}
)
return cast("type[StrictEnum]", enum_cls)
def _reused_annotated_enum(candidate: Any) -> type[StrictEnum] | None:
"""If `candidate` (a parameter/return annotation captured from a real
`Command[[ArgT], ResT]` class annotation, before init_command_from_introspection
would otherwise discard it) is already a genuine, concrete StrictEnum
subclass -- not the bare base class, not `inspect.Parameter.empty`/
`inspect.Signature.empty` -- return it so it can be reused verbatim
instead of building a fresh, differently-named class via
`_dynamic_enum_class`."""
if (
isinstance(candidate, type)
and issubclass(candidate, StrictEnum)
and candidate is not StrictEnum
):
return cast("type[StrictEnum]", candidate)
return None
[docs]
def python_type_to_str(python_type: Any) -> str:
"""Render a python type object as it should appear in generated source,
e.g. int -> "int", dict[str, Any] -> "dict[str, Any]"."""
if python_type in (None, type(None)):
return "None"
if isinstance(python_type, type):
return python_type.__name__
return str(python_type).replace("typing.", "")
[docs]
def command_dtype_to_annotation_str(datatype: DataType) -> str:
"""Render a SECoP command argument/result datatype as a python type string,
e.g. for use in a generated Command[[ArgT], ResT] annotation."""
return python_type_to_str(command_dtype_to_python_type(datatype))
[docs]
def build_command_signature(
cmd_datatype: CommandType,
annotated_signature: inspect.Signature | None = None,
) -> inspect.Signature:
"""Build the call signature for a SECoP command's argument(s)/result.
A StructOf argument is unraveled into one KEYWORD_ONLY parameter per
member (optional members default to None, matching frappy's own "None
means missing" convention for structs); any other argument datatype
becomes a single POSITIONAL_OR_KEYWORD 'arg' parameter; no argument means
no argument parameters. A trailing 'wait_for_idle: bool = False' is
always appended (KEYWORD_ONLY if the preceding param is, to avoid mixing
keyword-only and positional-or-keyword params). Used by
SECoPCommandBackend to build both its real call signature and to
validate/convert calls via Signature.bind().
`annotated_signature`, if given, is the signature captured from a real
`Command[[ArgT], ResT]` class annotation (before this function's caller
would otherwise discard it) -- when the bare (non-struct) argument or
result is an Enum and `annotated_signature` already carries a concrete
generated enum class there, that class is reused instead of building a
fresh, differently-named one via `_dynamic_enum_class`. Struct members
are unaffected: `annotated_signature` has a different shape there (a
single flat argument, not a per-member decomposition), and struct
members never get named enum classes at codegen time anyway.
"""
params: list[inspect.Parameter] = []
arg_dt = cmd_datatype.argument
annotated_params = (
list(annotated_signature.parameters.values()) if annotated_signature else []
)
if isinstance(arg_dt, StructOf):
for member_name, member_dtype in arg_dt.members.items():
annotation = (
_dynamic_enum_class(member_dtype, member_name)
if isinstance(member_dtype, EnumType)
else command_dtype_to_python_type(member_dtype)
)
params.append(
inspect.Parameter(
member_name,
inspect.Parameter.KEYWORD_ONLY,
annotation=annotation,
default=(
None
if member_name in arg_dt.optional
else inspect.Parameter.empty
),
)
)
elif arg_dt is not None:
if isinstance(arg_dt, EnumType):
reused = (
_reused_annotated_enum(annotated_params[0].annotation)
if annotated_params
else None
)
annotation = reused or _dynamic_enum_class(arg_dt, "arg")
else:
annotation = command_dtype_to_python_type(arg_dt)
params.append(
inspect.Parameter(
"arg",
inspect.Parameter.POSITIONAL_OR_KEYWORD,
annotation=annotation,
)
)
wait_for_idle_kind = (
inspect.Parameter.KEYWORD_ONLY
if params and params[-1].kind is inspect.Parameter.KEYWORD_ONLY
else inspect.Parameter.POSITIONAL_OR_KEYWORD
)
params.append(
inspect.Parameter(
"wait_for_idle", wait_for_idle_kind, annotation=bool, default=False
)
)
res_dt = cmd_datatype.result
return_annotation: type | None
if isinstance(res_dt, EnumType):
reused = (
_reused_annotated_enum(annotated_signature.return_annotation)
if annotated_signature
else None
)
return_annotation = reused or _dynamic_enum_class(res_dt, "result")
else:
return_annotation = (
command_dtype_to_python_type(res_dt) if res_dt is not None else None
)
return inspect.Signature(params, return_annotation=return_annotation)
[docs]
class SECoPReading:
def __init__(
self,
secop_dt: SECoPdtype,
entry: CacheItem | None = None,
) -> None:
self.secop_dt: SECoPdtype = secop_dt
if entry is None:
self.timestamp: float = time.time()
self.value = None
self.readerror = None
return
if entry.readerror is not None:
raise entry.readerror
# exported_val = secop_dt.raw_dtype.export_value(entry.value)
self.secop_dt.update_dtype(entry.value)
self.value = secop_dt.secop2val(entry.value)
self.secop_val = entry.value
self.timestamp = entry.timestamp
[docs]
def get_reading(self) -> Reading:
return {"value": self.value, "timestamp": self.timestamp}
[docs]
def get_value(self):
return self.value
[docs]
def get_secop_value(self):
return self.secop_val
[docs]
def set_reading(self, value) -> None:
self.value = value
self.secop_val = self.secop_dt.val2secop(value)
self.timestamp = time.time()
SECOP2DTYPE = {
FloatRange: (float, "number"),
IntRange: (int, "integer"),
ScaledInteger: (int, "integer"),
BoolType: (bool, "boolean"),
EnumType: (StrictEnum, "string"),
StringType: (str, "string"),
BLOBType: (str, "string"),
}