refdef 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- refdef/__init__.py +33 -0
- refdef/__main__.py +18 -0
- refdef/_api.py +42 -0
- refdef/_check.py +55 -0
- refdef/_heads.py +70 -0
- refdef/_parse.py +177 -0
- refdef/_render.py +64 -0
- refdef/designation.py +93 -0
- refdef/issues.py +47 -0
- refdef/level.py +111 -0
- refdef/numbering.py +42 -0
- refdef/order.py +44 -0
- refdef/plc.py +121 -0
- refdef/py.typed +0 -0
- refdef/scheme.py +77 -0
- refdef/tables/__init__.py +1 -0
- refdef/tables/standard.py +192 -0
- refdef/terminal.py +76 -0
- refdef-0.1.0.dist-info/METADATA +34 -0
- refdef-0.1.0.dist-info/RECORD +22 -0
- refdef-0.1.0.dist-info/WHEEL +4 -0
- refdef-0.1.0.dist-info/licenses/LICENSE +21 -0
refdef/level.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""One step of a reference designation: its aspect, class letters, number or label."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
MAX_NUMBER = 999_999_999
|
|
8
|
+
_NOT_IN_LABEL = frozenset("=-+%#:;&<>.")
|
|
9
|
+
_CODED = re.compile(r"([A-Za-z]*)([0-9]+)")
|
|
10
|
+
_LETTERS = re.compile(r"[A-Z]*")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Aspect(Enum):
|
|
14
|
+
"""The tree a level belongs to; the value is its sign.
|
|
15
|
+
|
|
16
|
+
The first three are on in the shipped scheme; declaration order is the rank tie-break.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
FUNCTION = "="
|
|
20
|
+
PRODUCT = "-"
|
|
21
|
+
LOCATION = "+"
|
|
22
|
+
TYPE = "%"
|
|
23
|
+
OTHER = "#"
|
|
24
|
+
ASSIGNMENT = "=="
|
|
25
|
+
SITE = "++"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def coded_split(body: str) -> tuple[str, str] | None:
|
|
29
|
+
"""Split `body` into letters and digits when it is shaped like a code, else None.
|
|
30
|
+
|
|
31
|
+
The digits must be worth 1 to 999_999_999; `K0` and `K1234567890` are not codes.
|
|
32
|
+
"""
|
|
33
|
+
hit = _CODED.fullmatch(body)
|
|
34
|
+
if hit is None:
|
|
35
|
+
return None
|
|
36
|
+
letters, digits = hit.groups()
|
|
37
|
+
significant = digits.lstrip("0") # int() of a 4300-digit run of zeros raises ValueError
|
|
38
|
+
if len(significant) > len(str(MAX_NUMBER)) or not 1 <= int(significant or "0") <= MAX_NUMBER:
|
|
39
|
+
return None
|
|
40
|
+
return letters, digits
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _check_types(aspect: object, code: object, label: object) -> None:
|
|
44
|
+
if not isinstance(aspect, Aspect):
|
|
45
|
+
msg = "aspect must be an Aspect"
|
|
46
|
+
raise TypeError(msg)
|
|
47
|
+
if not isinstance(code, str) or not isinstance(label, str):
|
|
48
|
+
msg = "code and label must be str"
|
|
49
|
+
raise TypeError(msg)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _check_number(number: object) -> None:
|
|
53
|
+
if number is None:
|
|
54
|
+
return
|
|
55
|
+
if not isinstance(number, int) or isinstance(number, bool):
|
|
56
|
+
msg = "number must be an int or None"
|
|
57
|
+
raise TypeError(msg)
|
|
58
|
+
if not 1 <= number <= MAX_NUMBER:
|
|
59
|
+
msg = f"number must be 1..{MAX_NUMBER}"
|
|
60
|
+
raise TypeError(msg)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _check_label(label: str) -> None:
|
|
64
|
+
if label != label.strip():
|
|
65
|
+
msg = "label must not have outer whitespace"
|
|
66
|
+
raise TypeError(msg)
|
|
67
|
+
if _NOT_IN_LABEL & set(label):
|
|
68
|
+
msg = "label must not hold any of = - + % # : ; & < > ."
|
|
69
|
+
raise TypeError(msg)
|
|
70
|
+
if coded_split(label) is not None:
|
|
71
|
+
msg = "label must not be shaped like a code (letters and a number)"
|
|
72
|
+
raise TypeError(msg)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True)
|
|
76
|
+
class Level:
|
|
77
|
+
"""One step in a tree: letters and a number (`-K2`), a number alone (`+3`) or a label (`+ER`).
|
|
78
|
+
|
|
79
|
+
Letters are upper case and need a number; a label is never code-shaped. Else TypeError.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
aspect: Aspect
|
|
83
|
+
code: str = ""
|
|
84
|
+
number: int | None = None
|
|
85
|
+
label: str = ""
|
|
86
|
+
|
|
87
|
+
def __post_init__(self) -> None:
|
|
88
|
+
"""Validate the shape; every violation raises TypeError naming the field."""
|
|
89
|
+
_check_types(self.aspect, self.code, self.label)
|
|
90
|
+
_check_number(self.number)
|
|
91
|
+
coded = bool(self.code) or self.number is not None
|
|
92
|
+
if coded == bool(self.label):
|
|
93
|
+
msg = "a level is letters and a number, a number alone, or a label"
|
|
94
|
+
raise TypeError(msg)
|
|
95
|
+
if coded:
|
|
96
|
+
self._check_coded()
|
|
97
|
+
else:
|
|
98
|
+
_check_label(self.label)
|
|
99
|
+
|
|
100
|
+
def _check_coded(self) -> None:
|
|
101
|
+
if _LETTERS.fullmatch(self.code) is None:
|
|
102
|
+
msg = "code must be upper-case ASCII letters A-Z only"
|
|
103
|
+
raise TypeError(msg)
|
|
104
|
+
if self.number is None:
|
|
105
|
+
msg = "code letters need a number"
|
|
106
|
+
raise TypeError(msg)
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def text(self) -> str:
|
|
110
|
+
"""The level without its sign: the label, or the letters and the number."""
|
|
111
|
+
return self.label or f"{self.code}{self.number}"
|
refdef/numbering.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Numbering: the lowest free number for a class, and a group of them in order."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Collection, Sequence
|
|
4
|
+
|
|
5
|
+
from refdef.level import Aspect, Level
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def allocate(
|
|
9
|
+
aspect: Aspect,
|
|
10
|
+
code: str,
|
|
11
|
+
taken: Collection[Level],
|
|
12
|
+
reserved: Collection[Level] = (),
|
|
13
|
+
*,
|
|
14
|
+
first: int = 1,
|
|
15
|
+
) -> int:
|
|
16
|
+
"""The smallest number from `first` for which `Level(aspect, code, n)` is in neither collection.
|
|
17
|
+
|
|
18
|
+
`first` below 1 is a TypeError. Nothing is stored: the caller passes what is taken.
|
|
19
|
+
"""
|
|
20
|
+
if not isinstance(first, int) or isinstance(first, bool) or first < 1:
|
|
21
|
+
msg = "first must be an int of at least 1"
|
|
22
|
+
raise TypeError(msg)
|
|
23
|
+
number = first
|
|
24
|
+
while Level(aspect, code, number) in taken or Level(aspect, code, number) in reserved:
|
|
25
|
+
number += 1
|
|
26
|
+
return number
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def number_group(
|
|
30
|
+
aspect: Aspect,
|
|
31
|
+
codes: Sequence[str],
|
|
32
|
+
taken: Collection[Level] = (),
|
|
33
|
+
reserved: Collection[Level] = (),
|
|
34
|
+
) -> tuple[Level, ...]:
|
|
35
|
+
"""One level per code, each numbered after the earlier ones; `taken` is not changed."""
|
|
36
|
+
used = set(taken)
|
|
37
|
+
out: list[Level] = []
|
|
38
|
+
for code in codes:
|
|
39
|
+
level = Level(aspect, code, allocate(aspect, code, used, reserved))
|
|
40
|
+
used.add(level)
|
|
41
|
+
out.append(level)
|
|
42
|
+
return tuple(out)
|
refdef/order.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""The natural order of designations: numbers by value, text as text."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from refdef.designation import Designation
|
|
8
|
+
from refdef.level import Aspect
|
|
9
|
+
|
|
10
|
+
_RUNS = re.compile(r"[0-9]+|[^0-9]+")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _run_key(run: str, *, case_sensitive: bool) -> tuple[Any, ...]:
|
|
14
|
+
if run.isascii() and run.isdigit():
|
|
15
|
+
significant = run.lstrip("0")
|
|
16
|
+
# (length, digits) orders like int(run) and never hits the int-from-text digit limit
|
|
17
|
+
return (0, len(significant), significant)
|
|
18
|
+
return (1, run if case_sensitive else run.lower())
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def natural_key(text: str, *, case_sensitive: bool = True) -> tuple[Any, ...]:
|
|
22
|
+
"""A sort key: digit runs by value, text runs as text, numbers first, then the raw text."""
|
|
23
|
+
if not isinstance(text, str):
|
|
24
|
+
msg = "text must be str"
|
|
25
|
+
raise TypeError(msg)
|
|
26
|
+
runs = tuple(_run_key(run, case_sensitive=case_sensitive) for run in _RUNS.findall(text))
|
|
27
|
+
return (runs, text)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def sort_key(
|
|
31
|
+
d: Designation, *, aspects: Sequence[Aspect], case_sensitive: bool = True
|
|
32
|
+
) -> tuple[Any, ...]:
|
|
33
|
+
"""A sort key: per level the rank of its aspect in `aspects` and its natural key.
|
|
34
|
+
|
|
35
|
+
Aspects not listed rank after the listed ones, in enum order. The top is not in the key.
|
|
36
|
+
"""
|
|
37
|
+
if not isinstance(aspects, Sequence) or not all(isinstance(a, Aspect) for a in aspects):
|
|
38
|
+
msg = "aspects must be a sequence of Aspect"
|
|
39
|
+
raise TypeError(msg)
|
|
40
|
+
order = [*aspects, *(a for a in Aspect if a not in aspects)]
|
|
41
|
+
return tuple(
|
|
42
|
+
(order.index(level.aspect), natural_key(level.text, case_sensitive=case_sensitive))
|
|
43
|
+
for level in d.levels
|
|
44
|
+
)
|
refdef/plc.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""The direct address of a PLC program (`%IX0.0`, `%QW4`, `%I*`) and a channel tag that pairs it."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass, replace
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
from refdef.issues import Issue, IssueCode, Parsed
|
|
8
|
+
from refdef.terminal import Terminal
|
|
9
|
+
|
|
10
|
+
__all__ = ["Address", "ChannelTag", "Memory", "Size", "parse_address"]
|
|
11
|
+
|
|
12
|
+
_MAX = 999_999_999
|
|
13
|
+
_DIGITS = 9
|
|
14
|
+
_ADDRESS = re.compile(r"%([IQM])([XBWDL]?)(\*|[0-9]+(?:\.[0-9]+)*)")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Memory(Enum):
|
|
18
|
+
"""The memory area of an address: input, output or marker; the value is the letter."""
|
|
19
|
+
|
|
20
|
+
INPUT = "I"
|
|
21
|
+
OUTPUT = "Q"
|
|
22
|
+
MARKER = "M"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Size(Enum):
|
|
26
|
+
"""The access size of an address: none, bit, byte, word, double word or long word."""
|
|
27
|
+
|
|
28
|
+
NONE = ""
|
|
29
|
+
BIT = "X"
|
|
30
|
+
BYTE = "B"
|
|
31
|
+
WORD = "W"
|
|
32
|
+
DWORD = "D"
|
|
33
|
+
LWORD = "L"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _is_index(value: object) -> bool:
|
|
37
|
+
"""True for an int (not a bool) from 0 to 999_999_999."""
|
|
38
|
+
return isinstance(value, int) and not isinstance(value, bool) and 0 <= value <= _MAX
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class Address:
|
|
43
|
+
"""A direct address printed `%` memory size path; an empty path is the incomplete `%I*`.
|
|
44
|
+
|
|
45
|
+
A path index is an int from 0 to 999_999_999; any other shape raises TypeError.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
memory: Memory
|
|
49
|
+
size: Size
|
|
50
|
+
path: tuple[int, ...]
|
|
51
|
+
|
|
52
|
+
def __post_init__(self) -> None:
|
|
53
|
+
"""Validate the fields; every violation raises TypeError."""
|
|
54
|
+
if not isinstance(self.memory, Memory) or not isinstance(self.size, Size):
|
|
55
|
+
msg = "memory must be a Memory and size a Size"
|
|
56
|
+
raise TypeError(msg)
|
|
57
|
+
if not isinstance(self.path, tuple) or not all(map(_is_index, self.path)):
|
|
58
|
+
msg = "path must be a tuple of ints from 0 to 999_999_999"
|
|
59
|
+
raise TypeError(msg)
|
|
60
|
+
|
|
61
|
+
def __str__(self) -> str:
|
|
62
|
+
"""The strict printed form."""
|
|
63
|
+
return f"%{self.memory.value}{self.size.value}" + (".".join(map(str, self.path)) or "*")
|
|
64
|
+
|
|
65
|
+
def as_bit(self) -> Address:
|
|
66
|
+
"""The one normalisation: a size-less dotted address gets the bit size; others are kept."""
|
|
67
|
+
if self.size is Size.NONE and len(self.path) > 1:
|
|
68
|
+
return replace(self, size=Size.BIT)
|
|
69
|
+
return self
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _element(part: str, issues: list[Issue]) -> str:
|
|
73
|
+
"""One path element without its leading zeros; the repair is an issue naming the element."""
|
|
74
|
+
stripped = part.lstrip("0") or "0"
|
|
75
|
+
if len(part) > 1 and part.startswith("0"):
|
|
76
|
+
issues.append(Issue(IssueCode.LEADING_ZERO, part, "leading zero dropped"))
|
|
77
|
+
return stripped
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def parse_address(text: str) -> Parsed[Address | None]:
|
|
81
|
+
"""Read a direct address; repairs (outer whitespace, leading zeros) are issues.
|
|
82
|
+
|
|
83
|
+
Text that is not an address gives `None` and a NOT_ADDRESS issue.
|
|
84
|
+
"""
|
|
85
|
+
if not isinstance(text, str):
|
|
86
|
+
msg = "text must be a str"
|
|
87
|
+
raise TypeError(msg)
|
|
88
|
+
body = text.strip()
|
|
89
|
+
match = _ADDRESS.fullmatch(body)
|
|
90
|
+
if match is None:
|
|
91
|
+
return Parsed(None, (Issue(IssueCode.NOT_ADDRESS, text, "not a direct address"),))
|
|
92
|
+
issues: list[Issue] = []
|
|
93
|
+
if body != text:
|
|
94
|
+
issues.append(Issue(IssueCode.WHITESPACE, text, "outer whitespace of the address removed"))
|
|
95
|
+
memory, size, rest = match.groups()
|
|
96
|
+
parts = [] if rest == "*" else [_element(part, issues) for part in rest.split(".")]
|
|
97
|
+
if any(len(part) > _DIGITS for part in parts):
|
|
98
|
+
return Parsed(None, (Issue(IssueCode.NOT_ADDRESS, text, "an index of over nine digits"),))
|
|
99
|
+
path = tuple(map(int, parts))
|
|
100
|
+
return Parsed(Address(Memory(memory), Size(size), path), tuple(issues))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass(frozen=True)
|
|
104
|
+
class ChannelTag:
|
|
105
|
+
"""A terminal with an optional address, printed as the terminal alone (`-KE1:DI03`)."""
|
|
106
|
+
|
|
107
|
+
terminal: Terminal
|
|
108
|
+
address: Address | None = None
|
|
109
|
+
|
|
110
|
+
def __post_init__(self) -> None:
|
|
111
|
+
"""Validate the fields; every violation raises TypeError."""
|
|
112
|
+
if not isinstance(self.terminal, Terminal):
|
|
113
|
+
msg = "terminal must be a Terminal"
|
|
114
|
+
raise TypeError(msg)
|
|
115
|
+
if self.address is not None and not isinstance(self.address, Address):
|
|
116
|
+
msg = "address must be None or an Address"
|
|
117
|
+
raise TypeError(msg)
|
|
118
|
+
|
|
119
|
+
def __str__(self) -> str:
|
|
120
|
+
"""The terminal text only."""
|
|
121
|
+
return str(self.terminal)
|
refdef/py.typed
ADDED
|
File without changes
|
refdef/scheme.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""The scheme a check reads: aspects that are on, a letter rule and a class table."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
from dataclasses import dataclass, replace
|
|
5
|
+
from functools import cached_property
|
|
6
|
+
|
|
7
|
+
from refdef.level import Aspect
|
|
8
|
+
from refdef.tables import standard as _standard
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class LetterRule:
|
|
13
|
+
"""How many letters a code has and which letters never appear."""
|
|
14
|
+
|
|
15
|
+
min_count: int = 1
|
|
16
|
+
max_count: int = 3
|
|
17
|
+
never: frozenset[str] = frozenset("IO")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class ClassEntry:
|
|
22
|
+
"""One class code with its title, the typical device of the class."""
|
|
23
|
+
|
|
24
|
+
code: str
|
|
25
|
+
title: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class ClassTable:
|
|
30
|
+
"""Versioned class entries; `depth` is how many leading letters of a code are looked up."""
|
|
31
|
+
|
|
32
|
+
name: str
|
|
33
|
+
version: str
|
|
34
|
+
depth: int
|
|
35
|
+
entries: tuple[ClassEntry, ...]
|
|
36
|
+
|
|
37
|
+
@cached_property
|
|
38
|
+
def index(self) -> dict[str, ClassEntry]:
|
|
39
|
+
"""The entries by code, built once."""
|
|
40
|
+
return {entry.code: entry for entry in self.entries}
|
|
41
|
+
|
|
42
|
+
def get(self, code: str) -> ClassEntry | None:
|
|
43
|
+
"""The entry for the first `depth` letters of `code`, or None; no shorter fallback."""
|
|
44
|
+
return self.index.get(code[: self.depth])
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class Scheme:
|
|
49
|
+
"""What `check` tests against: aspects on, a letter rule and a class table.
|
|
50
|
+
|
|
51
|
+
`STANDARD` is the shipped scheme: `= - +` on, 1-3 letters without I and O, a versioned table.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
name: str
|
|
55
|
+
aspects: frozenset[Aspect]
|
|
56
|
+
letters: LetterRule
|
|
57
|
+
classes: ClassTable
|
|
58
|
+
|
|
59
|
+
def extend(self, *entries: ClassEntry, aspects: Iterable[Aspect] = ()) -> Scheme:
|
|
60
|
+
"""A new scheme with `entries` overlaid by code (later wins) and `aspects` turned on."""
|
|
61
|
+
merged = {entry.code: entry for entry in (*self.classes.entries, *entries)}
|
|
62
|
+
table = replace(self.classes, entries=tuple(merged.values()))
|
|
63
|
+
return replace(self, aspects=self.aspects | frozenset(aspects), classes=table)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
_TABLE = ClassTable(
|
|
67
|
+
_standard.NAME,
|
|
68
|
+
_standard.VERSION,
|
|
69
|
+
_standard.DEPTH,
|
|
70
|
+
tuple(ClassEntry(code, title) for code, title in _standard.ENTRIES),
|
|
71
|
+
)
|
|
72
|
+
STANDARD = Scheme(
|
|
73
|
+
"standard",
|
|
74
|
+
frozenset({Aspect.FUNCTION, Aspect.PRODUCT, Aspect.LOCATION}),
|
|
75
|
+
LetterRule(),
|
|
76
|
+
_TABLE,
|
|
77
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Generated class tables."""
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Generated by scripts/gen_table.py; do not edit.
|
|
2
|
+
"""The generated class table."""
|
|
3
|
+
|
|
4
|
+
NAME = "standard"
|
|
5
|
+
VERSION = "1"
|
|
6
|
+
DEPTH = 2
|
|
7
|
+
ENTRIES: tuple[tuple[str, str], ...] = (
|
|
8
|
+
("B", "Sensors"),
|
|
9
|
+
("BA", "Voltage sensor"),
|
|
10
|
+
("BB", "Resistivity sensor"),
|
|
11
|
+
("BC", "Current sensor"),
|
|
12
|
+
("BD", "Density sensor"),
|
|
13
|
+
("BE", "Field sensor"),
|
|
14
|
+
("BF", "Flow sensor"),
|
|
15
|
+
("BG", "Position and distance sensors"),
|
|
16
|
+
("BH", "Energy meter"),
|
|
17
|
+
("BJ", "Power meter"),
|
|
18
|
+
("BK", "Timer"),
|
|
19
|
+
("BL", "Level sensor"),
|
|
20
|
+
("BM", "Humidity sensor"),
|
|
21
|
+
("BP", "Pressure sensor"),
|
|
22
|
+
("BQ", "Concentration sensors"),
|
|
23
|
+
("BR", "Light and radiation sensors"),
|
|
24
|
+
("BS", "Speed and rate sensors"),
|
|
25
|
+
("BT", "Temperature sensor"),
|
|
26
|
+
("BU", "Multi-quantity sensors"),
|
|
27
|
+
("BW", "Force and weight sensors"),
|
|
28
|
+
("BX", "Acoustic and image sensors"),
|
|
29
|
+
("BY", "Card and code readers"),
|
|
30
|
+
("BZ", "Counters and detectors"),
|
|
31
|
+
("C", "Storage devices"),
|
|
32
|
+
("CA", "Capacitor"),
|
|
33
|
+
("CB", "Inductor"),
|
|
34
|
+
("CC", "Battery"),
|
|
35
|
+
("CF", "Data storage"),
|
|
36
|
+
("CL", "Open storage"),
|
|
37
|
+
("CM", "Tanks and boxes"),
|
|
38
|
+
("CN", "Containers"),
|
|
39
|
+
("CP", "Heat storage"),
|
|
40
|
+
("CQ", "Flywheels and springs"),
|
|
41
|
+
("E", "Emitters"),
|
|
42
|
+
("EA", "Lamp"),
|
|
43
|
+
("EB", "Electric heater"),
|
|
44
|
+
("EC", "Electric cooler"),
|
|
45
|
+
("EE", "Wireless power source"),
|
|
46
|
+
("EG", "Heat pumps and exchangers"),
|
|
47
|
+
("EM", "Boilers and burners"),
|
|
48
|
+
("EP", "Radiators and heating coils"),
|
|
49
|
+
("EQ", "Chillers and coolers"),
|
|
50
|
+
("ET", "Nuclear reactor"),
|
|
51
|
+
("EU", "Particle sources"),
|
|
52
|
+
("EV", "Acoustic wave emitter"),
|
|
53
|
+
("F", "Protective devices"),
|
|
54
|
+
("FA", "Surge protectors"),
|
|
55
|
+
("FB", "Earth fault protectors"),
|
|
56
|
+
("FC", "Fuses and circuit breakers"),
|
|
57
|
+
("FE", "Field shields"),
|
|
58
|
+
("FL", "Pressure relief devices"),
|
|
59
|
+
("FM", "Fire protection devices"),
|
|
60
|
+
("FN", "Mechanical protectors"),
|
|
61
|
+
("FQ", "Guards and barriers"),
|
|
62
|
+
("FR", "Lubricants and wear pads"),
|
|
63
|
+
("FS", "Coatings and shelters"),
|
|
64
|
+
("G", "Generating devices"),
|
|
65
|
+
("GA", "Generator"),
|
|
66
|
+
("GB", "Batteries and fuel cells"),
|
|
67
|
+
("GC", "Photovoltaic module"),
|
|
68
|
+
("GF", "Signal generator"),
|
|
69
|
+
("GL", "Conveyors"),
|
|
70
|
+
("GM", "Cranes, lifts and wagons"),
|
|
71
|
+
("GP", "Pump"),
|
|
72
|
+
("GQ", "Fans and compressors"),
|
|
73
|
+
("GR", "Solar collector"),
|
|
74
|
+
("H", "Process machines"),
|
|
75
|
+
("HJ", "Casting and printing machines"),
|
|
76
|
+
("HK", "Surface treatment machines"),
|
|
77
|
+
("HL", "Assembly and joining machines"),
|
|
78
|
+
("HM", "Centrifuges and cyclones"),
|
|
79
|
+
("HP", "Dryers and distillers"),
|
|
80
|
+
("HQ", "Filters and screens"),
|
|
81
|
+
("HR", "Electromagnetic separators"),
|
|
82
|
+
("HS", "Ion exchangers and absorbers"),
|
|
83
|
+
("HU", "Mills and crushers"),
|
|
84
|
+
("HV", "Pelletizers and flocculators"),
|
|
85
|
+
("HW", "Mixers"),
|
|
86
|
+
("HX", "Reactors"),
|
|
87
|
+
("K", "Signal processing devices"),
|
|
88
|
+
("KE", "Computers and controllers"),
|
|
89
|
+
("KF", "Signal relay"),
|
|
90
|
+
("KG", "Optical network devices"),
|
|
91
|
+
("KH", "Fluid controllers"),
|
|
92
|
+
("KJ", "Mechanical regulators"),
|
|
93
|
+
("KZ", "Multi-signal devices"),
|
|
94
|
+
("M", "Drives"),
|
|
95
|
+
("MA", "Electric motor"),
|
|
96
|
+
("MB", "Linear actuators"),
|
|
97
|
+
("MC", "Permanent magnet"),
|
|
98
|
+
("MD", "Piezo actuator"),
|
|
99
|
+
("ML", "Turbines and spring drives"),
|
|
100
|
+
("MM", "Hydraulic and pneumatic drives"),
|
|
101
|
+
("MS", "Internal combustion engine"),
|
|
102
|
+
("MT", "Steam and Stirling engines"),
|
|
103
|
+
("N", "Covers"),
|
|
104
|
+
("NA", "Panels and seals"),
|
|
105
|
+
("NB", "Door leaves and hatch covers"),
|
|
106
|
+
("NC", "Surface finishes"),
|
|
107
|
+
("ND", "Edge trim"),
|
|
108
|
+
("NE", "Trim and flashing"),
|
|
109
|
+
("P", "Indicators and displays"),
|
|
110
|
+
("PF", "Signal lamp"),
|
|
111
|
+
("PG", "Meters and gauges"),
|
|
112
|
+
("PH", "Displays and signs"),
|
|
113
|
+
("PJ", "Loudspeakers and bells"),
|
|
114
|
+
("PK", "Tactile devices"),
|
|
115
|
+
("PL", "Ornaments"),
|
|
116
|
+
("PZ", "Audio-visual alarms"),
|
|
117
|
+
("Q", "Switching and control devices"),
|
|
118
|
+
("QA", "Switching device for power"),
|
|
119
|
+
("QB", "Disconnector"),
|
|
120
|
+
("QC", "Earthing switch"),
|
|
121
|
+
("QM", "Shutoff valves"),
|
|
122
|
+
("QN", "Control valves"),
|
|
123
|
+
("QP", "Open flow controllers"),
|
|
124
|
+
("QQ", "Doors, windows and gates"),
|
|
125
|
+
("QR", "Dosing valves"),
|
|
126
|
+
("QS", "Locks and latches"),
|
|
127
|
+
("QZ", "Earthing disconnector"),
|
|
128
|
+
("R", "Restrictors"),
|
|
129
|
+
("RA", "Resistors and diodes"),
|
|
130
|
+
("RB", "UPS and power filters"),
|
|
131
|
+
("RF", "Signal filters"),
|
|
132
|
+
("RL", "Brakes and shock absorbers"),
|
|
133
|
+
("RM", "Non-return valves"),
|
|
134
|
+
("RN", "Regulating valves and dampers"),
|
|
135
|
+
("RQ", "Insulation and screens"),
|
|
136
|
+
("RU", "Fences and barriers"),
|
|
137
|
+
("S", "Operator controls"),
|
|
138
|
+
("SF", "Eye and face readers"),
|
|
139
|
+
("SG", "Handles and hand switches"),
|
|
140
|
+
("SH", "Pedal switch"),
|
|
141
|
+
("SJ", "Pushbuttons and switches"),
|
|
142
|
+
("SK", "Joysticks and pointing devices"),
|
|
143
|
+
("SZ", "Operating panels"),
|
|
144
|
+
("T", "Converters"),
|
|
145
|
+
("TA", "Transformers and frequency converters"),
|
|
146
|
+
("TB", "Rectifiers and inverters"),
|
|
147
|
+
("TC", "AC/DC power supply"),
|
|
148
|
+
("TF", "Amplifiers and signal converters"),
|
|
149
|
+
("TL", "Gears and levers"),
|
|
150
|
+
("TM", "Cutting and drilling machines"),
|
|
151
|
+
("TP", "Forging and rolling machines"),
|
|
152
|
+
("TR", "Plants"),
|
|
153
|
+
("U", "Holders"),
|
|
154
|
+
("UA", "Mounting fixtures"),
|
|
155
|
+
("UB", "Cable trays and supports"),
|
|
156
|
+
("UC", "Cabinets and enclosures"),
|
|
157
|
+
("UL", "Structural members"),
|
|
158
|
+
("UM", "Reinforcement"),
|
|
159
|
+
("UN", "Frames"),
|
|
160
|
+
("UP", "Bearings and hinges"),
|
|
161
|
+
("UQ", "Fasteners"),
|
|
162
|
+
("UT", "Fill and excavation"),
|
|
163
|
+
("UU", "Soil and rock"),
|
|
164
|
+
("W", "Conductors and guides"),
|
|
165
|
+
("WB", "High voltage cables"),
|
|
166
|
+
("WD", "Low voltage cables"),
|
|
167
|
+
("WE", "Earthing conductors"),
|
|
168
|
+
("WG", "Signal cables"),
|
|
169
|
+
("WH", "Optical fibres and mirrors"),
|
|
170
|
+
("WJ", "Sound reflector"),
|
|
171
|
+
("WL", "Chutes and roller tables"),
|
|
172
|
+
("WM", "Gutters and drains"),
|
|
173
|
+
("WP", "Pipes and ducts"),
|
|
174
|
+
("WQ", "Shafts and drive belts"),
|
|
175
|
+
("WR", "Rail track"),
|
|
176
|
+
("WV", "Heat sinks"),
|
|
177
|
+
("WZ", "Umbilical cables"),
|
|
178
|
+
("X", "Connectors"),
|
|
179
|
+
("XB", "High voltage terminals and sockets"),
|
|
180
|
+
("XD", "Low voltage terminals and sockets"),
|
|
181
|
+
("XE", "Earthing terminals"),
|
|
182
|
+
("XG", "Signal sockets and patch panels"),
|
|
183
|
+
("XH", "Optical connectors"),
|
|
184
|
+
("XK", "Sinks and drains"),
|
|
185
|
+
("XM", "Pipe fittings and flanges"),
|
|
186
|
+
("XN", "Fixed couplings"),
|
|
187
|
+
("XP", "Clutches"),
|
|
188
|
+
("XS", "Stairs and ladders"),
|
|
189
|
+
("XT", "Openings"),
|
|
190
|
+
("XZ", "Multi-connector"),
|
|
191
|
+
)
|
|
192
|
+
ENTRIES_SHA256 = "dfe380f087194b2f27565793153c2d3c96f1e9161b68d23e942a7b27a69a51d6"
|
refdef/terminal.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""A terminal (`-K1:13`) and a signal (`-K1;SYSTEM_CAN_L`) of a designation."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from refdef.designation import Designation
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _stripped(text: object, banned: str) -> bool:
|
|
9
|
+
"""True when `text` is a non-empty str without outer whitespace and without `banned`."""
|
|
10
|
+
if not isinstance(text, str) or not text or text != text.strip():
|
|
11
|
+
return False
|
|
12
|
+
return not any(char in text for char in banned)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class Terminal:
|
|
17
|
+
"""A designation and the pins on it, printed `of:pin:pin`.
|
|
18
|
+
|
|
19
|
+
At least one pin; each is stripped, non-empty and without `:`. Pins are opaque text.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
of: Designation
|
|
23
|
+
pins: tuple[str, ...]
|
|
24
|
+
|
|
25
|
+
def __post_init__(self) -> None:
|
|
26
|
+
"""Validate the designation and the pins; every violation raises TypeError."""
|
|
27
|
+
if not isinstance(self.of, Designation):
|
|
28
|
+
msg = "of must be a Designation"
|
|
29
|
+
raise TypeError(msg)
|
|
30
|
+
if not isinstance(self.pins, tuple) or not self.pins:
|
|
31
|
+
msg = "pins must be a non-empty tuple of str"
|
|
32
|
+
raise TypeError(msg)
|
|
33
|
+
if not all(_stripped(pin, ":") for pin in self.pins):
|
|
34
|
+
msg = "each pin must be stripped, non-empty and without ':'"
|
|
35
|
+
raise TypeError(msg)
|
|
36
|
+
|
|
37
|
+
def __str__(self) -> str:
|
|
38
|
+
"""The strict printed form."""
|
|
39
|
+
return ":".join((str(self.of), *self.pins))
|
|
40
|
+
|
|
41
|
+
def relative_to(self, root: Designation) -> Terminal:
|
|
42
|
+
"""The terminal with its designation made relative to `root`; the pins are kept."""
|
|
43
|
+
return Terminal(self.of.relative_to(root), self.pins)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class Signal:
|
|
48
|
+
"""A signal name on a designation, printed `of;name`; `of` is None for a bare name.
|
|
49
|
+
|
|
50
|
+
The name is stripped and non-empty and may hold `: ; &`. It is opaque text.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
of: Designation | None
|
|
54
|
+
name: str
|
|
55
|
+
|
|
56
|
+
def __post_init__(self) -> None:
|
|
57
|
+
"""Validate the designation and the name; every violation raises TypeError."""
|
|
58
|
+
if self.of is not None and (
|
|
59
|
+
not isinstance(self.of, Designation) or not (self.of.levels or self.of.top)
|
|
60
|
+
):
|
|
61
|
+
msg = "of must be None or a Designation with a level or a top"
|
|
62
|
+
raise TypeError(msg)
|
|
63
|
+
if not _stripped(self.name, ""):
|
|
64
|
+
msg = "name must be a stripped, non-empty str"
|
|
65
|
+
raise TypeError(msg)
|
|
66
|
+
|
|
67
|
+
def __str__(self) -> str:
|
|
68
|
+
"""The strict printed form."""
|
|
69
|
+
return (str(self.of) if self.of is not None else "") + ";" + self.name
|
|
70
|
+
|
|
71
|
+
def relative_to(self, root: Designation) -> Signal:
|
|
72
|
+
"""The signal with its designation made relative to `root`; None if nothing is left."""
|
|
73
|
+
if self.of is None:
|
|
74
|
+
return self
|
|
75
|
+
rest = self.of.relative_to(root)
|
|
76
|
+
return Signal(rest if rest.levels or rest.top else None, self.name)
|