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.
Files changed (60) hide show
  1. elecdef/__init__.py +51 -0
  2. elecdef/__main__.py +18 -0
  3. elecdef/_api.py +56 -0
  4. elecdef/_block.py +56 -0
  5. elecdef/_body.py +25 -0
  6. elecdef/_breaker.py +62 -0
  7. elecdef/_check.py +62 -0
  8. elecdef/_checkpins.py +84 -0
  9. elecdef/_checkwalk.py +26 -0
  10. elecdef/_coil.py +52 -0
  11. elecdef/_connection.py +28 -0
  12. elecdef/_contact.py +69 -0
  13. elecdef/_device.py +36 -0
  14. elecdef/_issue.py +26 -0
  15. elecdef/_numbering.py +29 -0
  16. elecdef/_overload.py +76 -0
  17. elecdef/_plccard.py +71 -0
  18. elecdef/_product.py +54 -0
  19. elecdef/_psu.py +74 -0
  20. elecdef/_quantity.py +160 -0
  21. elecdef/_range.py +176 -0
  22. elecdef/_rating.py +26 -0
  23. elecdef/_relay.py +121 -0
  24. elecdef/cable.py +102 -0
  25. elecdef/computer.py +61 -0
  26. elecdef/conductor.py +30 -0
  27. elecdef/connections.py +34 -0
  28. elecdef/connector.py +56 -0
  29. elecdef/fuse.py +91 -0
  30. elecdef/pins.py +158 -0
  31. elecdef/plc.py +124 -0
  32. elecdef/py.typed +0 -0
  33. elecdef/supply.py +26 -0
  34. elecdef/tables/HELD.txt +20 -0
  35. elecdef/tables/__init__.py +27 -0
  36. elecdef/tables/awg.py +98 -0
  37. elecdef/tables/breaker_currents.py +22 -0
  38. elecdef/tables/breaker_curves.py +10 -0
  39. elecdef/tables/breaker_icn.py +13 -0
  40. elecdef/tables/cable_voltages.py +12 -0
  41. elecdef/tables/core_colours.py +51 -0
  42. elecdef/tables/cross_sections.py +35 -0
  43. elecdef/tables/e_series.py +50 -0
  44. elecdef/tables/overvoltage_impulse.py +12 -0
  45. elecdef/tables/trip_times.py +11 -0
  46. elecdef/terminal.py +87 -0
  47. elecdef/vocab/__init__.py +118 -0
  48. elecdef/vocab/_base.py +17 -0
  49. elecdef/vocab/_cable.py +92 -0
  50. elecdef/vocab/_coil.py +53 -0
  51. elecdef/vocab/_conditions.py +51 -0
  52. elecdef/vocab/_connection.py +84 -0
  53. elecdef/vocab/_contact.py +30 -0
  54. elecdef/vocab/_misc.py +85 -0
  55. elecdef/vocab/_plc.py +105 -0
  56. elecdef/vocab/_protection.py +93 -0
  57. elecdef-0.1.0.dist-info/METADATA +34 -0
  58. elecdef-0.1.0.dist-info/RECORD +60 -0
  59. elecdef-0.1.0.dist-info/WHEEL +4 -0
  60. 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