elecdef 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.
- elecdef/__init__.py +51 -0
- elecdef/__main__.py +18 -0
- elecdef/_api.py +56 -0
- elecdef/_block.py +56 -0
- elecdef/_body.py +25 -0
- elecdef/_breaker.py +62 -0
- elecdef/_check.py +62 -0
- elecdef/_checkpins.py +84 -0
- elecdef/_checkwalk.py +26 -0
- elecdef/_coil.py +52 -0
- elecdef/_connection.py +28 -0
- elecdef/_contact.py +69 -0
- elecdef/_device.py +36 -0
- elecdef/_issue.py +26 -0
- elecdef/_numbering.py +29 -0
- elecdef/_overload.py +76 -0
- elecdef/_plccard.py +71 -0
- elecdef/_product.py +54 -0
- elecdef/_psu.py +74 -0
- elecdef/_quantity.py +160 -0
- elecdef/_range.py +176 -0
- elecdef/_rating.py +26 -0
- elecdef/_relay.py +121 -0
- elecdef/cable.py +102 -0
- elecdef/computer.py +61 -0
- elecdef/conductor.py +30 -0
- elecdef/connections.py +34 -0
- elecdef/connector.py +56 -0
- elecdef/fuse.py +91 -0
- elecdef/pins.py +158 -0
- elecdef/plc.py +124 -0
- elecdef/py.typed +0 -0
- elecdef/supply.py +26 -0
- elecdef/tables/HELD.txt +20 -0
- elecdef/tables/__init__.py +27 -0
- elecdef/tables/awg.py +98 -0
- elecdef/tables/breaker_currents.py +22 -0
- elecdef/tables/breaker_curves.py +10 -0
- elecdef/tables/breaker_icn.py +13 -0
- elecdef/tables/cable_voltages.py +12 -0
- elecdef/tables/core_colours.py +51 -0
- elecdef/tables/cross_sections.py +35 -0
- elecdef/tables/e_series.py +50 -0
- elecdef/tables/overvoltage_impulse.py +12 -0
- elecdef/tables/trip_times.py +11 -0
- elecdef/terminal.py +87 -0
- elecdef/vocab/__init__.py +118 -0
- elecdef/vocab/_base.py +17 -0
- elecdef/vocab/_cable.py +92 -0
- elecdef/vocab/_coil.py +53 -0
- elecdef/vocab/_conditions.py +51 -0
- elecdef/vocab/_connection.py +84 -0
- elecdef/vocab/_contact.py +30 -0
- elecdef/vocab/_misc.py +85 -0
- elecdef/vocab/_plc.py +105 -0
- elecdef/vocab/_protection.py +93 -0
- elecdef-0.1.0.dist-info/METADATA +34 -0
- elecdef-0.1.0.dist-info/RECORD +60 -0
- elecdef-0.1.0.dist-info/WHEEL +4 -0
- elecdef-0.1.0.dist-info/licenses/LICENSE +21 -0
elecdef/__init__.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Standard-agnostic electrical engineering vocabulary as strict, typed, immutable values."""
|
|
2
|
+
|
|
3
|
+
from elecdef._body import Body
|
|
4
|
+
from elecdef._breaker import Breaker
|
|
5
|
+
from elecdef._check import check
|
|
6
|
+
from elecdef._coil import Coil
|
|
7
|
+
from elecdef._connection import Connection
|
|
8
|
+
from elecdef._contact import Contact
|
|
9
|
+
from elecdef._device import Device
|
|
10
|
+
from elecdef._issue import Issue, IssueCode
|
|
11
|
+
from elecdef._overload import Overload
|
|
12
|
+
from elecdef._plccard import PlcCard
|
|
13
|
+
from elecdef._product import Product, Symbol
|
|
14
|
+
from elecdef._psu import PowerSupply
|
|
15
|
+
from elecdef._quantity import Quantity, Unit
|
|
16
|
+
from elecdef._range import At, Pick, Range, Ranges, Role, pick
|
|
17
|
+
from elecdef._rating import Rating
|
|
18
|
+
from elecdef._relay import ContactBlock, Contactor, Relay
|
|
19
|
+
from elecdef.fuse import Fuse
|
|
20
|
+
from elecdef.terminal import TerminalBlock
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"At",
|
|
24
|
+
"Body",
|
|
25
|
+
"Breaker",
|
|
26
|
+
"Coil",
|
|
27
|
+
"Connection",
|
|
28
|
+
"Contact",
|
|
29
|
+
"ContactBlock",
|
|
30
|
+
"Contactor",
|
|
31
|
+
"Device",
|
|
32
|
+
"Fuse",
|
|
33
|
+
"Issue",
|
|
34
|
+
"IssueCode",
|
|
35
|
+
"Overload",
|
|
36
|
+
"Pick",
|
|
37
|
+
"PlcCard",
|
|
38
|
+
"PowerSupply",
|
|
39
|
+
"Product",
|
|
40
|
+
"Quantity",
|
|
41
|
+
"Range",
|
|
42
|
+
"Ranges",
|
|
43
|
+
"Rating",
|
|
44
|
+
"Relay",
|
|
45
|
+
"Role",
|
|
46
|
+
"Symbol",
|
|
47
|
+
"TerminalBlock",
|
|
48
|
+
"Unit",
|
|
49
|
+
"check",
|
|
50
|
+
"pick",
|
|
51
|
+
]
|
elecdef/__main__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Command line: `python -m elecdef api` prints the public surface."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from elecdef._api import DOORS, render
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main(argv: list[str]) -> int:
|
|
9
|
+
"""Run the one command, `api`; any other argument is a usage error (exit 2)."""
|
|
10
|
+
if argv != ["api"]:
|
|
11
|
+
sys.stderr.write("usage: python -m elecdef api\n")
|
|
12
|
+
return 2
|
|
13
|
+
sys.stdout.write(render(DOORS))
|
|
14
|
+
return 0
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
if __name__ == "__main__":
|
|
18
|
+
sys.exit(main(sys.argv[1:]))
|
elecdef/_api.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""The surface printer: `render` lists every door's `__all__` with signatures and docstrings."""
|
|
2
|
+
|
|
3
|
+
import annotationlib
|
|
4
|
+
import dataclasses
|
|
5
|
+
import enum
|
|
6
|
+
import importlib
|
|
7
|
+
import inspect
|
|
8
|
+
|
|
9
|
+
DOORS: tuple[str, ...] = (
|
|
10
|
+
"elecdef",
|
|
11
|
+
"elecdef.vocab",
|
|
12
|
+
"elecdef.connections",
|
|
13
|
+
"elecdef.pins",
|
|
14
|
+
"elecdef.supply",
|
|
15
|
+
"elecdef.plc",
|
|
16
|
+
"elecdef.tables",
|
|
17
|
+
"elecdef.terminal",
|
|
18
|
+
"elecdef.fuse",
|
|
19
|
+
"elecdef.conductor",
|
|
20
|
+
"elecdef.cable",
|
|
21
|
+
"elecdef.connector",
|
|
22
|
+
"elecdef.computer",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _signature(obj: object) -> str:
|
|
27
|
+
"""The signature as source text, so 3.14 and 3.15 print the same bytes."""
|
|
28
|
+
return str(inspect.signature(obj, annotation_format=annotationlib.Format.STRING)) # ty: ignore[invalid-argument-type] - obj is callable here
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _head(name: str, obj: object) -> str:
|
|
32
|
+
"""The first line of a name's entry."""
|
|
33
|
+
if isinstance(obj, type) and issubclass(obj, enum.Enum):
|
|
34
|
+
return f"class {name}"
|
|
35
|
+
if isinstance(obj, type) and dataclasses.is_dataclass(obj):
|
|
36
|
+
return f"class {name}{_signature(obj)}"
|
|
37
|
+
if callable(obj):
|
|
38
|
+
return f"{name}{_signature(obj)}"
|
|
39
|
+
return f"{name}: {type(obj).__name__}"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _entry(name: str, obj: object) -> list[str]:
|
|
43
|
+
"""A name's head, its docstring indented four spaces, and a blank line."""
|
|
44
|
+
doc = inspect.getdoc(obj if callable(obj) else type(obj)) or ""
|
|
45
|
+
return [_head(name, obj), *(f" {line}".rstrip() for line in doc.splitlines()), ""]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def render(doors: tuple[str, ...]) -> str:
|
|
49
|
+
"""Return the public surface of `doors` as text: deterministic, one final newline."""
|
|
50
|
+
lines: list[str] = []
|
|
51
|
+
for door in doors:
|
|
52
|
+
module = importlib.import_module(door)
|
|
53
|
+
lines.append(f"## {door}")
|
|
54
|
+
for name in sorted(module.__all__):
|
|
55
|
+
lines += _entry(name, getattr(module, name))
|
|
56
|
+
return "\n".join(lines).rstrip("\n") + "\n"
|
elecdef/_block.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""The one base class of the blocks: it checks the kind of each figure and wraps a bare Range."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field, fields
|
|
4
|
+
from typing import Any, ClassVar
|
|
5
|
+
|
|
6
|
+
from elecdef._quantity import Kind, Quantity
|
|
7
|
+
from elecdef._range import Range
|
|
8
|
+
|
|
9
|
+
__all__ = ["Block", "figure"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def figure(*kinds: Kind, many: bool = False) -> Any: # noqa: ANN401 - dataclasses types field() as Any
|
|
13
|
+
"""A block field that holds a figure of one of `kinds`; `many` holds a tuple of Range."""
|
|
14
|
+
return field(default=() if many else None, metadata={"kinds": kinds, "many": many})
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _kind_of(value: object) -> Kind:
|
|
18
|
+
"""The kind of a Quantity or a Range; any other value is a TypeError."""
|
|
19
|
+
if isinstance(value, Quantity):
|
|
20
|
+
return value.unit.kind
|
|
21
|
+
if isinstance(value, Range):
|
|
22
|
+
return value.kind
|
|
23
|
+
message = f"a figure is a Quantity or a Range, not {type(value).__name__}"
|
|
24
|
+
raise TypeError(message)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _check_figure(name: str, value: object, kinds: tuple[Kind, ...]) -> None:
|
|
28
|
+
"""Refuse a filled figure whose kind is not one of `kinds`."""
|
|
29
|
+
if value is not None and _kind_of(value) not in kinds:
|
|
30
|
+
message = f"{name} takes {[kind.name for kind in kinds]}, not {_kind_of(value).name}"
|
|
31
|
+
raise TypeError(message)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class Block:
|
|
36
|
+
"""The base of every block: refuses a figure of another kind at construction."""
|
|
37
|
+
|
|
38
|
+
needs: ClassVar[tuple[str, ...]] = ()
|
|
39
|
+
|
|
40
|
+
def __post_init__(self) -> None:
|
|
41
|
+
"""Check each filled figure and wrap a bare Range given to a `many` field."""
|
|
42
|
+
for item in fields(self):
|
|
43
|
+
if "kinds" not in item.metadata:
|
|
44
|
+
continue
|
|
45
|
+
value = getattr(self, item.name)
|
|
46
|
+
if not item.metadata["many"]:
|
|
47
|
+
_check_figure(item.name, value, item.metadata["kinds"])
|
|
48
|
+
continue
|
|
49
|
+
if isinstance(value, Range):
|
|
50
|
+
value = (value,)
|
|
51
|
+
object.__setattr__(self, item.name, value)
|
|
52
|
+
if not isinstance(value, tuple):
|
|
53
|
+
message = f"{item.name} is a tuple of Range"
|
|
54
|
+
raise TypeError(message)
|
|
55
|
+
for each in value:
|
|
56
|
+
_check_figure(item.name, each, item.metadata["kinds"])
|
elecdef/_body.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""The Body block: the outer size, protection, temperature and mounting of a part."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from elecdef._block import Block, figure
|
|
6
|
+
from elecdef._quantity import Kind, Quantity
|
|
7
|
+
from elecdef._range import Ranges
|
|
8
|
+
from elecdef.vocab import IpCode, Mounting
|
|
9
|
+
|
|
10
|
+
__all__ = ["Body"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class Body(Block):
|
|
15
|
+
"""The physical body of a part.
|
|
16
|
+
|
|
17
|
+
Temperature rows tell operating (RATED) from storage (LIMITING) by their role.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
width: Quantity | None = figure(Kind.LENGTH)
|
|
21
|
+
height: Quantity | None = figure(Kind.LENGTH)
|
|
22
|
+
depth: Quantity | None = figure(Kind.LENGTH)
|
|
23
|
+
ip: IpCode | None = None
|
|
24
|
+
temperature: Ranges = figure(Kind.TEMPERATURE, many=True)
|
|
25
|
+
mounting: Mounting | None = None
|
elecdef/_breaker.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""The Breaker block: pole pins as a fuse holder has them, then auxiliaries numbered AUX."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import ClassVar
|
|
5
|
+
|
|
6
|
+
from elecdef._block import Block, figure
|
|
7
|
+
from elecdef._body import Body
|
|
8
|
+
from elecdef._connection import Connection
|
|
9
|
+
from elecdef._contact import Contact
|
|
10
|
+
from elecdef._numbering import _joined, _numbered
|
|
11
|
+
from elecdef._product import Symbol
|
|
12
|
+
from elecdef._quantity import Kind
|
|
13
|
+
from elecdef._range import Range
|
|
14
|
+
from elecdef._rating import Rating
|
|
15
|
+
from elecdef.fuse import pole_pins
|
|
16
|
+
from elecdef.pins import Link, Marking, Pin
|
|
17
|
+
from elecdef.vocab import TripCurve
|
|
18
|
+
|
|
19
|
+
__all__ = ["Breaker"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class Breaker(Block):
|
|
24
|
+
"""A circuit breaker: current, trip curve and breaking capacities, pole pins, auxiliaries.
|
|
25
|
+
|
|
26
|
+
Pins and links are computed on every read, so `replace(breaker, poles=1)` leaves none stale.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
needs: ClassVar[tuple[str, ...]] = ("current", "curve")
|
|
30
|
+
|
|
31
|
+
poles: int | None = None
|
|
32
|
+
current: Range | None = figure(Kind.CURRENT)
|
|
33
|
+
curve: TripCurve | None = None
|
|
34
|
+
icn: Range | None = figure(Kind.CURRENT)
|
|
35
|
+
icu: Range | None = figure(Kind.CURRENT)
|
|
36
|
+
ics: Range | None = figure(Kind.CURRENT)
|
|
37
|
+
rating: Rating | None = None
|
|
38
|
+
aux: tuple[Contact, ...] = ()
|
|
39
|
+
connection: Connection | None = None
|
|
40
|
+
body: Body | None = None
|
|
41
|
+
symbol: Symbol | None = None
|
|
42
|
+
|
|
43
|
+
def __post_init__(self) -> None:
|
|
44
|
+
"""Check the figures, then read the pins once so a bad pole count fails here."""
|
|
45
|
+
super().__post_init__()
|
|
46
|
+
_ = self.pins
|
|
47
|
+
|
|
48
|
+
def _joined(self) -> tuple[tuple[Pin, ...], tuple[Link, ...]]:
|
|
49
|
+
"""The pole pins and links, then those of the auxiliaries."""
|
|
50
|
+
pole_p, pole_l = pole_pins(self.poles)
|
|
51
|
+
pins, links = _joined(None, _numbered(self.aux, Marking.AUX))
|
|
52
|
+
return pole_p + pins, pole_l + links
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def pins(self) -> tuple[Pin, ...]:
|
|
56
|
+
"""The pole pins, then the pins of each auxiliary contact."""
|
|
57
|
+
return self._joined()[0]
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def links(self) -> tuple[Link, ...]:
|
|
61
|
+
"""One protective link per pole, then the links of the auxiliaries."""
|
|
62
|
+
return self._joined()[1]
|
elecdef/_check.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""`check`: the content issues of a block or a Product, reported as values and never raised."""
|
|
2
|
+
|
|
3
|
+
from elecdef._block import Block
|
|
4
|
+
from elecdef._checkpins import duplicate_issues, link_issues, marking_issues
|
|
5
|
+
from elecdef._checkwalk import join, walk
|
|
6
|
+
from elecdef._issue import Issue, IssueCode
|
|
7
|
+
from elecdef._product import Product
|
|
8
|
+
from elecdef._range import Range
|
|
9
|
+
|
|
10
|
+
__all__ = ["check"]
|
|
11
|
+
|
|
12
|
+
_ORDER = (("min", "nom"), ("min", "typ"), ("min", "max"), ("nom", "max"), ("typ", "max"))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _range_issues(path: str, item: Range) -> list[Issue]:
|
|
16
|
+
"""END_ORDER for a Range whose ends are not in order."""
|
|
17
|
+
wrong = [
|
|
18
|
+
f"{low} is above {high}"
|
|
19
|
+
for low, high in _ORDER
|
|
20
|
+
if getattr(item, low) is not None
|
|
21
|
+
and getattr(item, high) is not None
|
|
22
|
+
and getattr(item, low) > getattr(item, high)
|
|
23
|
+
]
|
|
24
|
+
return [Issue(IssueCode.END_ORDER, "; ".join(wrong), path)] if wrong else []
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _block_issues(path: str, block: Block, *, product: bool) -> list[Issue]:
|
|
28
|
+
"""UNKNOWN_CLASS, the marking and link issues, and MISSING_FIGURE inside a Product."""
|
|
29
|
+
found: list[Issue] = []
|
|
30
|
+
module = type(block).__module__
|
|
31
|
+
if module != "elecdef" and not module.startswith("elecdef."):
|
|
32
|
+
message = f"{type(block).__qualname__} is not defined in the elecdef catalogue"
|
|
33
|
+
found.append(Issue(IssueCode.UNKNOWN_CLASS, message, path or "block"))
|
|
34
|
+
found += marking_issues(path, block) + link_issues(path, block)
|
|
35
|
+
if product:
|
|
36
|
+
found.extend(
|
|
37
|
+
Issue(IssueCode.MISSING_FIGURE, f"{name} is needed and unfilled", join(path, name))
|
|
38
|
+
for name in type(block).needs
|
|
39
|
+
if getattr(block, name) in (None, ())
|
|
40
|
+
)
|
|
41
|
+
return found
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def check(block: Block | Product) -> tuple[Issue, ...]:
|
|
45
|
+
"""Report the content issues of a block, or of the block inside a Product, as a tuple.
|
|
46
|
+
|
|
47
|
+
A figure a block kind needs is reported only inside a Product; a bare block is a template.
|
|
48
|
+
"""
|
|
49
|
+
if isinstance(block, Product):
|
|
50
|
+
root, product = block.block, True
|
|
51
|
+
elif isinstance(block, Block):
|
|
52
|
+
root, product = block, False
|
|
53
|
+
else:
|
|
54
|
+
message = f"check takes a Block or a Product, not {type(block).__name__}"
|
|
55
|
+
raise TypeError(message)
|
|
56
|
+
found: list[Issue] = []
|
|
57
|
+
for path, item in walk(root):
|
|
58
|
+
if isinstance(item, Range):
|
|
59
|
+
found += _range_issues(path, item)
|
|
60
|
+
else:
|
|
61
|
+
found += _block_issues(path, item, product=product)
|
|
62
|
+
return tuple(found + duplicate_issues(root))
|
elecdef/_checkpins.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""The pin issues of `check`: marking syntax once per authored pin, uniqueness over the device."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import fields
|
|
4
|
+
|
|
5
|
+
from elecdef._block import Block
|
|
6
|
+
from elecdef._checkwalk import join, walk
|
|
7
|
+
from elecdef._issue import Issue, IssueCode
|
|
8
|
+
from elecdef.pins import Pin, _is_marking
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _pin_field(block: Block) -> str | None:
|
|
12
|
+
"""The attribute that holds the pins the author gave, or a leaf's computed `pins`."""
|
|
13
|
+
names = {item.name for item in fields(block)}
|
|
14
|
+
if "own_pins" in names:
|
|
15
|
+
return "own_pins"
|
|
16
|
+
if "pins" in names:
|
|
17
|
+
return "pins"
|
|
18
|
+
leaf = all(item is block for _, item in walk(block) if isinstance(item, Block))
|
|
19
|
+
return "pins" if leaf and hasattr(block, "pins") else None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def authored(path: str, block: Block) -> list[tuple[str, Pin]]:
|
|
23
|
+
"""Each pin the block authors, with its subject path; a composite's computed pins are not."""
|
|
24
|
+
name = _pin_field(block)
|
|
25
|
+
if name is None:
|
|
26
|
+
return []
|
|
27
|
+
return [(f"{join(path, name)}[{index}]", pin) for index, pin in enumerate(getattr(block, name))]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def marking_issues(path: str, block: Block) -> list[Issue]:
|
|
31
|
+
"""BAD_MARKING for each authored pin whose marking is not a run of marking characters."""
|
|
32
|
+
return [
|
|
33
|
+
Issue(
|
|
34
|
+
IssueCode.BAD_MARKING,
|
|
35
|
+
f"pin marking {pin.marking!r} is not a run of marking characters",
|
|
36
|
+
subject,
|
|
37
|
+
)
|
|
38
|
+
for subject, pin in authored(path, block)
|
|
39
|
+
if not _is_marking(pin.marking)
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def link_issues(path: str, block: Block) -> list[Issue]:
|
|
44
|
+
"""DANGLING_LINK for each link marking that names no pin of the block."""
|
|
45
|
+
names = {pin.marking for pin in getattr(block, "pins", ())}
|
|
46
|
+
return [
|
|
47
|
+
Issue(
|
|
48
|
+
IssueCode.DANGLING_LINK,
|
|
49
|
+
f"link names {marking!r}, which is no pin",
|
|
50
|
+
f"{join(path, 'links')}[{index}]",
|
|
51
|
+
)
|
|
52
|
+
for index, link in enumerate(getattr(block, "links", ()))
|
|
53
|
+
for marking in link.markings
|
|
54
|
+
if marking not in names
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _subjects(root: Block, pins: tuple[Pin, ...]) -> list[str]:
|
|
59
|
+
"""The subject of each pin in the root's full pin set: its authored path, else `pins[i]`."""
|
|
60
|
+
pool = [
|
|
61
|
+
pair
|
|
62
|
+
for path, item in walk(root)
|
|
63
|
+
if isinstance(item, Block)
|
|
64
|
+
for pair in authored(path, item)
|
|
65
|
+
]
|
|
66
|
+
subjects: list[str] = []
|
|
67
|
+
for index, pin in enumerate(pins):
|
|
68
|
+
match = next((at for at, (_, given) in enumerate(pool) if given == pin), None)
|
|
69
|
+
subjects.append(pool.pop(match)[0] if match is not None else f"pins[{index}]")
|
|
70
|
+
return subjects
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def duplicate_issues(root: Block) -> list[Issue]:
|
|
74
|
+
"""DUPLICATE_MARKING for each repeat after the first over the outermost block's full pins."""
|
|
75
|
+
pins = tuple(getattr(root, "pins", ()))
|
|
76
|
+
found: list[Issue] = []
|
|
77
|
+
seen: set[tuple[str, object]] = set()
|
|
78
|
+
for pin, subject in zip(pins, _subjects(root, pins), strict=True):
|
|
79
|
+
if (pin.marking, pin.side) in seen:
|
|
80
|
+
side = f" on side {pin.side.name}" if pin.side else ""
|
|
81
|
+
message = f"pin marking {pin.marking!r} is repeated{side}"
|
|
82
|
+
found.append(Issue(IssueCode.DUPLICATE_MARKING, message, subject))
|
|
83
|
+
seen.add((pin.marking, pin.side))
|
|
84
|
+
return found
|
elecdef/_checkwalk.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""The generic walk `check` makes: every Block and Range under a value, with its path."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
from dataclasses import fields, is_dataclass
|
|
5
|
+
|
|
6
|
+
from elecdef._block import Block
|
|
7
|
+
from elecdef._range import Range
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def join(path: str, name: str) -> str:
|
|
11
|
+
"""The path of `name` under `path`."""
|
|
12
|
+
return f"{path}.{name}" if path else name
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def walk(value: object, path: str = "") -> Iterator[tuple[str, Block | Range]]:
|
|
16
|
+
"""Each Block and Range under `value`, parents first, through dataclass fields and tuples."""
|
|
17
|
+
if isinstance(value, Range):
|
|
18
|
+
yield path, value
|
|
19
|
+
elif isinstance(value, tuple):
|
|
20
|
+
for index, each in enumerate(value):
|
|
21
|
+
yield from walk(each, f"{path}[{index}]")
|
|
22
|
+
elif is_dataclass(value) and not isinstance(value, type):
|
|
23
|
+
if isinstance(value, Block):
|
|
24
|
+
yield path, value
|
|
25
|
+
for item in fields(value):
|
|
26
|
+
yield from walk(getattr(value, item.name), join(path, item.name))
|
elecdef/_coil.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""The Coil block: the drive of a relay or contactor, with its A1 and A2 pins."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import ClassVar
|
|
5
|
+
|
|
6
|
+
from elecdef._block import Block, figure
|
|
7
|
+
from elecdef._connection import Connection
|
|
8
|
+
from elecdef._product import Symbol
|
|
9
|
+
from elecdef._quantity import Kind
|
|
10
|
+
from elecdef._range import Range, Ranges
|
|
11
|
+
from elecdef.pins import Link, Pin, PinRole
|
|
12
|
+
from elecdef.vocab import CoilKind, Suppression
|
|
13
|
+
|
|
14
|
+
__all__ = ["Coil"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class Coil(Block):
|
|
19
|
+
"""One coil: kind, drive figures, suppression, and the pins computed from kind and suppression.
|
|
20
|
+
|
|
21
|
+
`own_pins` holds only pins the author gave; with none, `pins` is A1 and A2 read on every access.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
needs: ClassVar[tuple[str, ...]] = ("voltage",)
|
|
25
|
+
|
|
26
|
+
kind: CoilKind | None = None
|
|
27
|
+
voltage: Ranges = figure(Kind.VOLTAGE, many=True)
|
|
28
|
+
pick_up: Range | None = figure(Kind.RATIO)
|
|
29
|
+
drop_out: Range | None = figure(Kind.RATIO)
|
|
30
|
+
resistance: Range | None = figure(Kind.RESISTANCE)
|
|
31
|
+
power: Ranges = figure(Kind.POWER, many=True)
|
|
32
|
+
apparent_power: Ranges = figure(Kind.APPARENT_POWER, many=True)
|
|
33
|
+
operate_time: Range | None = figure(Kind.TIME)
|
|
34
|
+
release_time: Range | None = figure(Kind.TIME)
|
|
35
|
+
suppression: Suppression | None = None
|
|
36
|
+
connection: Connection | None = None
|
|
37
|
+
own_pins: tuple[Pin, ...] = ()
|
|
38
|
+
symbol: Symbol | None = None
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def pins(self) -> tuple[Pin, ...]:
|
|
42
|
+
"""The author's `own_pins`, else A1 and A2 (PLUS and MINUS for a DC coil with a diode)."""
|
|
43
|
+
if self.own_pins:
|
|
44
|
+
return self.own_pins
|
|
45
|
+
polar = self.kind is CoilKind.DC and self.suppression is Suppression.DIODE
|
|
46
|
+
first, second = (PinRole.PLUS, PinRole.MINUS) if polar else (PinRole.COIL, PinRole.COIL)
|
|
47
|
+
return Pin("A1", first), Pin("A2", second)
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def links(self) -> tuple[Link, ...]:
|
|
51
|
+
"""A coil joins no pins."""
|
|
52
|
+
return ()
|
elecdef/_connection.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""The Connection block: how a conductor or a pin is attached."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from elecdef._block import Block, figure
|
|
6
|
+
from elecdef._quantity import Kind, Quantity
|
|
7
|
+
from elecdef._range import Range, Ranges
|
|
8
|
+
from elecdef.vocab import ConductorMaterial, ScrewSize, Technology
|
|
9
|
+
|
|
10
|
+
__all__ = ["Connection"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class Connection(Block):
|
|
15
|
+
"""One connection point: its technology, conductor range, strip length and torque.
|
|
16
|
+
|
|
17
|
+
Every field is unfilled by default, so a leaf states only its technology.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
technology: Technology | None = None
|
|
21
|
+
conductor: Ranges = figure(Kind.AREA, many=True)
|
|
22
|
+
conductor_awg: Ranges = figure(Kind.GAUGE, many=True)
|
|
23
|
+
conductors_per_clamp: int | None = None
|
|
24
|
+
strip_length: Quantity | None = figure(Kind.LENGTH)
|
|
25
|
+
torque: Range | None = figure(Kind.TORQUE)
|
|
26
|
+
screw: ScrewSize | None = None
|
|
27
|
+
pitch: Quantity | None = figure(Kind.LENGTH)
|
|
28
|
+
material: ConductorMaterial | None = None
|
elecdef/_contact.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""The Contact block: one switching element, with the pins its number and scheme give."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import ClassVar
|
|
5
|
+
|
|
6
|
+
from elecdef._block import Block, figure
|
|
7
|
+
from elecdef._connection import Connection
|
|
8
|
+
from elecdef._product import Symbol
|
|
9
|
+
from elecdef._quantity import Kind
|
|
10
|
+
from elecdef._range import Range, Ranges
|
|
11
|
+
from elecdef.pins import Link, Marking, Pin, contact_pins
|
|
12
|
+
from elecdef.vocab import ContactFeature, ContactForm, ContactMaterial
|
|
13
|
+
|
|
14
|
+
__all__ = ["Contact"]
|
|
15
|
+
|
|
16
|
+
_MAX_N = 9
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class Contact(Block):
|
|
21
|
+
"""One contact: form, ratings per condition, and the pins computed from number and scheme.
|
|
22
|
+
|
|
23
|
+
`own_pins` holds only what the author gave; `pins` is read from `number` and `scheme` each time.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
needs: ClassVar[tuple[str, ...]] = ("switching",)
|
|
27
|
+
|
|
28
|
+
form: ContactForm | None = None
|
|
29
|
+
features: frozenset[ContactFeature] = field(default_factory=frozenset)
|
|
30
|
+
switching: Ranges = figure(Kind.CURRENT, many=True)
|
|
31
|
+
voltage: Range | None = figure(Kind.VOLTAGE)
|
|
32
|
+
min_load_voltage: Range | None = figure(Kind.VOLTAGE)
|
|
33
|
+
min_load_current: Range | None = figure(Kind.CURRENT)
|
|
34
|
+
material: ContactMaterial | None = None
|
|
35
|
+
mechanical_life: int | None = None
|
|
36
|
+
electrical_life: int | None = None
|
|
37
|
+
connection: Connection | None = None
|
|
38
|
+
number: int | None = None
|
|
39
|
+
scheme: Marking | None = None
|
|
40
|
+
own_pins: tuple[Pin, ...] = ()
|
|
41
|
+
symbol: Symbol | None = None
|
|
42
|
+
|
|
43
|
+
def __post_init__(self) -> None:
|
|
44
|
+
"""Check the figures, then refuse a number or form the scheme cannot mark."""
|
|
45
|
+
super().__post_init__()
|
|
46
|
+
number = self.number
|
|
47
|
+
if number is not None and (
|
|
48
|
+
not isinstance(number, int) or isinstance(number, bool) or not 1 <= number <= _MAX_N
|
|
49
|
+
):
|
|
50
|
+
message = "number is an int from 1 to 9"
|
|
51
|
+
raise TypeError(message)
|
|
52
|
+
self._computed()
|
|
53
|
+
|
|
54
|
+
def _computed(self) -> tuple[tuple[Pin, ...], tuple[Link, ...]]:
|
|
55
|
+
"""The pins and links read from form, number and scheme; empty when any is unset."""
|
|
56
|
+
form, number, scheme = self.form, self.number, self.scheme
|
|
57
|
+
if self.own_pins or form is None or number is None or scheme is None:
|
|
58
|
+
return (), ()
|
|
59
|
+
return contact_pins(form, number, scheme)
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def pins(self) -> tuple[Pin, ...]:
|
|
63
|
+
"""The author's `own_pins`, else the pins of the contact's form, number and scheme."""
|
|
64
|
+
return self.own_pins or self._computed()[0]
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def links(self) -> tuple[Link, ...]:
|
|
68
|
+
"""The links of the computed pins; none with `own_pins`."""
|
|
69
|
+
return self._computed()[1]
|
elecdef/_device.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""The Device block: pins and links given outright, the typed fallback for a thing with no block."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from elecdef._block import Block
|
|
6
|
+
from elecdef._body import Body
|
|
7
|
+
from elecdef._connection import Connection
|
|
8
|
+
from elecdef._product import Symbol
|
|
9
|
+
from elecdef._rating import Rating
|
|
10
|
+
from elecdef.pins import Link, Pin
|
|
11
|
+
|
|
12
|
+
__all__ = ["Device"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class Device(Block):
|
|
17
|
+
"""A thing with no block of its own yet: its pins and links are stored as given.
|
|
18
|
+
|
|
19
|
+
Every field is unfilled by default.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
pins: tuple[Pin, ...] = ()
|
|
23
|
+
links: tuple[Link, ...] = ()
|
|
24
|
+
rating: Rating | None = None
|
|
25
|
+
connection: Connection | None = None
|
|
26
|
+
body: Body | None = None
|
|
27
|
+
symbol: Symbol | None = None
|
|
28
|
+
|
|
29
|
+
def __post_init__(self) -> None:
|
|
30
|
+
"""Refuse pins or links that are not a tuple of Pin or Link."""
|
|
31
|
+
super().__post_init__()
|
|
32
|
+
for name, kind in (("pins", Pin), ("links", Link)):
|
|
33
|
+
value = getattr(self, name)
|
|
34
|
+
if not (isinstance(value, tuple) and all(isinstance(each, kind) for each in value)):
|
|
35
|
+
message = f"{name} is a tuple of {kind.__name__}"
|
|
36
|
+
raise TypeError(message)
|
elecdef/_issue.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Issues as values: what `check` and `pick` report instead of raising."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum, auto
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class IssueCode(Enum):
|
|
8
|
+
"""The closed set of issue codes; `check` reports the first six, `pick` the last two."""
|
|
9
|
+
|
|
10
|
+
END_ORDER = auto()
|
|
11
|
+
DUPLICATE_MARKING = auto()
|
|
12
|
+
BAD_MARKING = auto()
|
|
13
|
+
DANGLING_LINK = auto()
|
|
14
|
+
MISSING_FIGURE = auto()
|
|
15
|
+
UNKNOWN_CLASS = auto()
|
|
16
|
+
NO_RATING = auto()
|
|
17
|
+
AMBIGUOUS_RATING = auto()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class Issue:
|
|
22
|
+
"""One finding: a code, a message and the subject path it concerns, such as `coil.voltage`."""
|
|
23
|
+
|
|
24
|
+
code: IssueCode
|
|
25
|
+
message: str
|
|
26
|
+
subject: str
|