Source code for semi_cr.core.lab.station.graphing.parsing
from dataclasses import dataclass
from typing import Any
import re
_ENDPOINT_RE = re.compile(r"^(?P<instrument>[^\[]+)\[(?P<channel>.+)\]$")
[docs]
@dataclass(frozen=True)
class Endpoint:
owner: str
terminal: str
[docs]
def is_endpoint_ref(value: str) -> bool:
return "[" in value and value.endswith("]")
[docs]
def parse_endpoint_ref(endpoint: str) -> tuple[str, str]:
owner, rest = endpoint.split("[", 1)
name = rest.removesuffix("]")
return owner, name
def _strip_matching_quotes(value: str) -> str:
value = value.strip()
if (
len(value) >= 2
and value[0] == value[-1]
and value[0] in {"'", '"'}
):
return value[1:-1]
return value
[docs]
def parse_endpoint(
endpoint: str,
) -> tuple[str, str]:
if not isinstance(endpoint, str):
raise TypeError(
f"Endpoint must be a string, got "
f"{type(endpoint).__name__}."
)
endpoint = endpoint.strip()
if not endpoint.endswith("]") or "[" not in endpoint:
raise ValueError(
f"Invalid endpoint {endpoint!r}. Expected "
"'instrument[channel]'."
)
instrument_name, channel = (
endpoint[:-1].split("[", maxsplit=1)
)
instrument_name = instrument_name.strip()
channel = _strip_matching_quotes(channel)
if not instrument_name or not channel:
raise ValueError(
f"Invalid endpoint {endpoint!r}: instrument "
"and channel must be non-empty."
)
return instrument_name, channel
# def parse_endpoint(endpoint: str) -> Endpoint:
# ...
[docs]
def normalize_channel(
channel: str | int,
) -> str:
"""
Normalize a terminal channel path.
Examples
--------
1 -> "1"
"ch01" -> "ch01"
"LF_1[out1]" -> "LF_1/out1"
"""
if isinstance(channel, bool):
raise TypeError(
"Channel must be a string or integer, got bool."
)
if isinstance(channel, int):
channel = str(channel)
elif not isinstance(channel, str):
raise TypeError(
f"Channel must be a string or integer, got "
f"{type(channel).__name__}."
)
channel = channel.strip()
if not channel:
raise ValueError("Terminal path cannot be empty.")
has_opening = "[" in channel
has_closing = "]" in channel
if has_opening != has_closing:
raise ValueError(
f"Invalid terminal path {channel!r}: "
"unmatched brackets."
)
if not has_opening:
return channel
if channel.count("[") != 1 or channel.count("]") != 1:
raise ValueError(
f"Invalid terminal path {channel!r}: "
"expected one bracketed channel."
)
if not channel.endswith("]"):
raise ValueError(
f"Invalid terminal path {channel!r}: "
"closing bracket must be at the end."
)
module_name, terminal_name = (
channel[:-1].split("[", maxsplit=1)
)
if not module_name or not terminal_name:
raise ValueError(
f"Invalid terminal path {channel!r}: "
"module and terminal names must be non-empty."
)
return f"{module_name}/{terminal_name}"
[docs]
def parse_channel_number(name: str) -> int | None:
match = re.search(r"(\d+)$", name)
return int(match.group(1)) if match else None