bytesim 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 (48) hide show
  1. bytesim/__init__.py +14 -0
  2. bytesim/__main__.py +3 -0
  3. bytesim/cli.py +45 -0
  4. bytesim/constants.py +48 -0
  5. bytesim/demos.py +74 -0
  6. bytesim/model/__init__.py +5 -0
  7. bytesim/model/addressing.py +160 -0
  8. bytesim/model/base.py +57 -0
  9. bytesim/model/cloning.py +249 -0
  10. bytesim/model/equivalence.py +86 -0
  11. bytesim/model/groups.py +133 -0
  12. bytesim/model/ha.py +93 -0
  13. bytesim/model/layer2.py +106 -0
  14. bytesim/model/layout.py +203 -0
  15. bytesim/model/naming.py +114 -0
  16. bytesim/model/network.py +138 -0
  17. bytesim/model/routing.py +117 -0
  18. bytesim/model/stp.py +315 -0
  19. bytesim/model/types.py +107 -0
  20. bytesim/py.typed +0 -0
  21. bytesim/scripting.py +108 -0
  22. bytesim/synthesis.py +644 -0
  23. bytesim/topology.py +517 -0
  24. bytesim/tui/__init__.py +4 -0
  25. bytesim/tui/app.py +161 -0
  26. bytesim/tui/cables.py +58 -0
  27. bytesim/tui/canvas.py +489 -0
  28. bytesim/tui/command_input.py +29 -0
  29. bytesim/tui/commands/__init__.py +1 -0
  30. bytesim/tui/commands/addressing.py +235 -0
  31. bytesim/tui/commands/base.py +43 -0
  32. bytesim/tui/commands/groups.py +215 -0
  33. bytesim/tui/commands/history.py +134 -0
  34. bytesim/tui/commands/interpreter.py +85 -0
  35. bytesim/tui/commands/naming.py +68 -0
  36. bytesim/tui/commands/registry.py +28 -0
  37. bytesim/tui/commands/scripting.py +54 -0
  38. bytesim/tui/commands/switching.py +192 -0
  39. bytesim/tui/commands/topology.py +255 -0
  40. bytesim/tui/commands/traffic.py +312 -0
  41. bytesim/tui/help.py +50 -0
  42. bytesim/tui/inspector.py +115 -0
  43. bytesim/tui/theme.py +12 -0
  44. bytesim-0.1.0.dist-info/METADATA +178 -0
  45. bytesim-0.1.0.dist-info/RECORD +48 -0
  46. bytesim-0.1.0.dist-info/WHEEL +4 -0
  47. bytesim-0.1.0.dist-info/entry_points.txt +2 -0
  48. bytesim-0.1.0.dist-info/licenses/LICENSE +21 -0
bytesim/model/types.py ADDED
@@ -0,0 +1,107 @@
1
+ """Plain data: devices, cables, interface addresses."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+
6
+ from ..constants import BOX_H, KINDS
7
+
8
+
9
+ class NetError(Exception):
10
+ pass
11
+
12
+
13
+ def fmt(addr: tuple[int, int] | None) -> str:
14
+ return f"({addr[0]},{addr[1]})" if addr else "—"
15
+
16
+
17
+ @dataclass
18
+ class Iface:
19
+ """Logical address of one port/NIC: (network id, 8-bit host).
20
+
21
+ mode: "dhcp" (computers' default), "adhoc" (pick a free host, no server),
22
+ "static", "auto" (router/gateway ports), or "none".
23
+ via: how the current address was actually obtained.
24
+ """
25
+ mode: str
26
+ net: int | None = None
27
+ host: int | None = None
28
+ gw: int | None = None # gateway's host number, on the same network
29
+ via: str = ""
30
+
31
+ @property
32
+ def addr(self) -> tuple[int, int] | None:
33
+ return (self.net, self.host) if self.host is not None and self.net is not None else None
34
+
35
+ def clear(self) -> None:
36
+ self.net = self.host = self.gw = None
37
+ self.via = ""
38
+
39
+
40
+ Leg = tuple[tuple[str, int], tuple[str, int], list[tuple[str, int]]]
41
+
42
+
43
+ @dataclass
44
+ class Device:
45
+ kind: str
46
+ num: int
47
+ nports: int
48
+ x: int = 0
49
+ y: int = 0
50
+ up: bool = True
51
+
52
+ @property
53
+ def id(self) -> str:
54
+ return f"{self.kind}{self.num}"
55
+
56
+ @property
57
+ def name(self) -> str:
58
+ return KINDS[self.kind][0]
59
+
60
+ @property
61
+ def color(self) -> str:
62
+ return KINDS[self.kind][2]
63
+
64
+ @property
65
+ def width(self) -> int:
66
+ return max(len(self.id) + 2, self.nports + 2) + 2
67
+
68
+ def port_x(self, port: int) -> int:
69
+ inner = self.width - 2
70
+ return self.x + 1 + (inner - self.nports) // 2 + (port - 1)
71
+
72
+ def port_name(self, port: int) -> str:
73
+ return f"nic{port}" if self.kind == "c" else f"p{port}"
74
+
75
+ def mac(self, port: int) -> str:
76
+ """Device id plus a port letter: R1A, R1B, ... Z, AA, AB ..."""
77
+ letters, n = "", port
78
+ while n:
79
+ n, r = divmod(n - 1, 26)
80
+ letters = chr(65 + r) + letters
81
+ return self.id.upper() + letters
82
+
83
+ def contains(self, x: int, y: int) -> bool:
84
+ return self.x <= x < self.x + self.width and self.y <= y < self.y + BOX_H
85
+
86
+
87
+ @dataclass
88
+ class Link:
89
+ a: str
90
+ pa: int
91
+ b: str
92
+ pb: int
93
+ role: str = "data" # data | control | control-backup | fabric
94
+ up: bool = True
95
+
96
+ @property
97
+ def control(self) -> bool:
98
+ return self.role.startswith("control")
99
+
100
+ def touches(self, dev_id: str) -> bool:
101
+ return dev_id in (self.a, self.b)
102
+
103
+ def other(self, dev_id: str) -> tuple[str, int]:
104
+ return (self.b, self.pb) if dev_id == self.a else (self.a, self.pa)
105
+
106
+ def port_of(self, dev_id: str) -> int:
107
+ return self.pa if dev_id == self.a else self.pb
bytesim/py.typed ADDED
File without changes
bytesim/scripting.py ADDED
@@ -0,0 +1,108 @@
1
+ """Run ByteSim command scripts without the UI.
2
+
3
+ The runner uses the *same* command handlers and interpreter as the app; only the
4
+ screen is replaced by a stub. It builds structure (devices, groups, cables,
5
+ settings) and skips what the screen and the address model would do after each
6
+ command, which a script doesn't need to reproduce its structure.
7
+
8
+ from bytesim.scripting import replay
9
+ net = replay(open("site.txt").read())
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from collections import Counter
14
+ from types import SimpleNamespace
15
+
16
+ from .model import NetError, Network
17
+ from .tui.commands.addressing import AddressCommands
18
+ from .tui.commands.groups import GroupCommands
19
+ from .tui.commands.interpreter import Interpreter
20
+ from .tui.commands.naming import NamingCommands
21
+ from .tui.commands.registry import collect
22
+ from .tui.commands.switching import SwitchingCommands
23
+ from .tui.commands.topology import TopologyCommands
24
+ from .tui.commands.traffic import TrafficCommands
25
+
26
+
27
+ class _Screen:
28
+ """Stands in for the canvas: remembers the selection, finishes animations at once."""
29
+
30
+ def __init__(self) -> None:
31
+ self.selected: str | None = None
32
+ self.selected_group: str | None = None
33
+ self.pan = (0, 0)
34
+ self.packets: list = []
35
+ self.flash: dict = {}
36
+ self.hot_links: Counter = Counter()
37
+ self.size = SimpleNamespace(width=120, height=40)
38
+ self.busy = False
39
+
40
+ def select(self, dev_id: str | None) -> None:
41
+ self.selected, self.selected_group = dev_id, None
42
+
43
+ def select_group(self, name: str) -> None:
44
+ self.selected, self.selected_group = None, name
45
+
46
+ def send_packet(self, cells, color, on_done=None) -> None:
47
+ if on_done:
48
+ on_done()
49
+
50
+ def focus(self, *a) -> None: ...
51
+ def animate(self) -> None: ...
52
+ def refresh(self, *a, **k) -> None: ...
53
+ def reset(self) -> None: ...
54
+ def heat(self, links) -> None: ...
55
+ def cool(self, links) -> None: ...
56
+ def flash_device(self, *a, **k) -> None: ...
57
+
58
+
59
+ class ScriptRunner(TopologyCommands, GroupCommands, AddressCommands, TrafficCommands, NamingCommands,
60
+ SwitchingCommands, Interpreter):
61
+ """A headless ByteSim: feed it commands, read back ``net``."""
62
+
63
+ def __init__(self, network: Network | None = None) -> None:
64
+ self.net = network or Network()
65
+ self.auto = False
66
+ self.graph = False
67
+ self.replaying = 1 # always "in a script": no per-command side effects
68
+ self.naming = None
69
+ self.log: list[str] = []
70
+ self._ha_seen: dict = {}
71
+ self._screen = _Screen()
72
+ self._commands = collect(type(self))
73
+
74
+ @property
75
+ def canvas(self): # type: ignore[override]
76
+ return self._screen
77
+
78
+ def log_msg(self, msg: str) -> None:
79
+ self.log.append(msg)
80
+
81
+ def changed(self, skip=frozenset()) -> None: # structure only: no layout, no re-addressing
82
+ pass
83
+
84
+ def refresh_sidebar(self) -> None:
85
+ pass
86
+
87
+ def replace_network(self, net: Network, auto: bool) -> None:
88
+ self.net = net
89
+
90
+ def arrange(self, announce: bool = False) -> None:
91
+ pass
92
+
93
+ def exit(self) -> None:
94
+ pass
95
+
96
+ def run(self, text: str, origin: str = "script") -> Network:
97
+ """Run every command; raises NetError naming the line that failed."""
98
+ for n, line in self.script_lines(text):
99
+ try:
100
+ self.dispatch(self.tokens(line))
101
+ except NetError as e:
102
+ raise NetError(f"{origin}, line {n} ({line}): {e}") from None
103
+ return self.net
104
+
105
+
106
+ def replay(text: str, network: Network | None = None) -> Network:
107
+ """Build a network by running a script (into a fresh network unless one is given)."""
108
+ return ScriptRunner(network).run(text)