testprotocols 0.6.0__tar.gz → 0.7.1__tar.gz

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 (145) hide show
  1. {testprotocols-0.6.0/src/testprotocols.egg-info → testprotocols-0.7.1}/PKG-INFO +1 -1
  2. {testprotocols-0.6.0 → testprotocols-0.7.1}/pyproject.toml +1 -1
  3. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/__init__.py +1 -1
  4. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/first_hop_security.py +1 -3
  5. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/iperf_client.py +0 -5
  6. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/models/dhcp.py +1 -1
  7. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/models/sdwan_appliance.py +11 -11
  8. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/models/switch.py +3 -3
  9. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/models/switch_routing.py +7 -7
  10. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/sdwan_policy_manager.py +3 -1
  11. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/site_to_site_vpn.py +32 -3
  12. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/switch_acl.py +1 -3
  13. {testprotocols-0.6.0 → testprotocols-0.7.1/src/testprotocols.egg-info}/PKG-INFO +1 -1
  14. testprotocols-0.7.1/tests/test_appliance_vlans.py +52 -0
  15. {testprotocols-0.6.0 → testprotocols-0.7.1}/tests/test_cpe_templates.py +2 -1
  16. {testprotocols-0.6.0 → testprotocols-0.7.1}/tests/test_device_types.py +81 -46
  17. {testprotocols-0.6.0 → testprotocols-0.7.1}/tests/test_dhcp_templates.py +3 -2
  18. {testprotocols-0.6.0 → testprotocols-0.7.1}/tests/test_firewall_templates.py +5 -4
  19. {testprotocols-0.6.0 → testprotocols-0.7.1}/tests/test_infra_templates.py +2 -1
  20. {testprotocols-0.6.0 → testprotocols-0.7.1}/tests/test_ip_interface.py +2 -1
  21. {testprotocols-0.6.0 → testprotocols-0.7.1}/tests/test_ip_routing.py +2 -1
  22. {testprotocols-0.6.0 → testprotocols-0.7.1}/tests/test_network_tool_templates.py +2 -1
  23. {testprotocols-0.6.0 → testprotocols-0.7.1}/tests/test_radius_templates.py +2 -1
  24. {testprotocols-0.6.0 → testprotocols-0.7.1}/tests/test_sdwan_appliance_models.py +16 -13
  25. {testprotocols-0.6.0 → testprotocols-0.7.1}/tests/test_sdwan_appliance_templates.py +4 -1
  26. {testprotocols-0.6.0 → testprotocols-0.7.1}/tests/test_switch_models.py +19 -18
  27. {testprotocols-0.6.0 → testprotocols-0.7.1}/tests/test_switch_routing_models.py +12 -7
  28. testprotocols-0.7.1/tests/test_switch_templates.py +95 -0
  29. {testprotocols-0.6.0 → testprotocols-0.7.1}/tests/test_traffic_templates.py +2 -1
  30. {testprotocols-0.6.0 → testprotocols-0.7.1}/tests/test_voice_wifi_templates.py +2 -1
  31. {testprotocols-0.6.0 → testprotocols-0.7.1}/tests/test_wan_edge_templates.py +4 -3
  32. {testprotocols-0.6.0 → testprotocols-0.7.1}/tests/test_wifi_templates.py +6 -5
  33. testprotocols-0.6.0/tests/test_appliance_vlans.py +0 -33
  34. testprotocols-0.6.0/tests/test_switch_templates.py +0 -69
  35. {testprotocols-0.6.0 → testprotocols-0.7.1}/LICENSE +0 -0
  36. {testprotocols-0.6.0 → testprotocols-0.7.1}/NOTICE +0 -0
  37. {testprotocols-0.6.0 → testprotocols-0.7.1}/README.md +0 -0
  38. {testprotocols-0.6.0 → testprotocols-0.7.1}/setup.cfg +0 -0
  39. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/aftr_gateway.py +0 -0
  40. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/appliance_nat.py +0 -0
  41. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/appliance_uplinks.py +0 -0
  42. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/appliance_vlans.py +0 -0
  43. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/arp_client.py +0 -0
  44. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/bgp.py +0 -0
  45. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/conntrack.py +0 -0
  46. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/content_filtering.py +0 -0
  47. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/device_lifecycle.py +0 -0
  48. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/device_management.py +0 -0
  49. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/devices/__init__.py +0 -0
  50. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/devices/base.py +0 -0
  51. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/devices/client.py +0 -0
  52. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/devices/cpe.py +0 -0
  53. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/devices/infra.py +0 -0
  54. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/devices/sdwan.py +0 -0
  55. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/devices/switch.py +0 -0
  56. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/devices/traffic.py +0 -0
  57. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/devices/voice.py +0 -0
  58. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/devices/wan.py +0 -0
  59. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/dhcp_client.py +0 -0
  60. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/dhcp_server.py +0 -0
  61. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/discovery.py +0 -0
  62. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/dns_client.py +0 -0
  63. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/file_transfer.py +0 -0
  64. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/firewall.py +0 -0
  65. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/firewall_zones.py +0 -0
  66. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/gateway_redundancy.py +0 -0
  67. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/http_client.py +0 -0
  68. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/http_server.py +0 -0
  69. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/hw_console.py +0 -0
  70. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/interface_dhcp.py +0 -0
  71. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/ip_interface.py +0 -0
  72. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/ip_routing.py +0 -0
  73. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/iperf_generator.py +0 -0
  74. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/iperf_server.py +0 -0
  75. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/l3_firewall.py +0 -0
  76. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/l7_firewall.py +0 -0
  77. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/link_aggregation.py +0 -0
  78. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/mac_table.py +0 -0
  79. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/models/__init__.py +1 -1
  80. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/models/firewall.py +0 -0
  81. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/models/impairment.py +0 -0
  82. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/models/l2_common.py +0 -0
  83. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/models/multicast.py +0 -0
  84. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/models/networking.py +0 -0
  85. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/models/packets.py +0 -0
  86. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/models/qoe.py +0 -0
  87. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/models/radius.py +0 -0
  88. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/models/tr069.py +0 -0
  89. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/models/traffic.py +0 -0
  90. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/models/wan_edge.py +0 -0
  91. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/models/wifi.py +0 -0
  92. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/multicast_client.py +0 -0
  93. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/nat.py +0 -0
  94. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/netem_controller.py +0 -0
  95. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/network_endpoint.py +0 -0
  96. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/network_probe.py +0 -0
  97. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/nmap_scanner.py +0 -0
  98. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/ntp_client.py +0 -0
  99. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/ntp_config.py +0 -0
  100. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/ospf.py +0 -0
  101. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/packet_filter.py +0 -0
  102. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/pcap_capture.py +0 -0
  103. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/pdu_controller.py +0 -0
  104. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/port_poe.py +0 -0
  105. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/port_security.py +0 -0
  106. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/port_status.py +0 -0
  107. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/py.typed +0 -0
  108. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/qoe_browser.py +0 -0
  109. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/radius_client.py +0 -0
  110. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/radius_server.py +0 -0
  111. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/reachability_responder.py +0 -0
  112. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/routed_interfaces.py +0 -0
  113. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/router.py +0 -0
  114. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/routing_read.py +0 -0
  115. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/sip_phone.py +0 -0
  116. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/sip_server.py +0 -0
  117. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/snmp_client.py +0 -0
  118. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/spanning_tree.py +0 -0
  119. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/static_routes.py +0 -0
  120. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/storm_control.py +0 -0
  121. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/streaming_server.py +0 -0
  122. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/switch_ports.py +0 -0
  123. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/switch_qos.py +0 -0
  124. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/switch_vlans.py +0 -0
  125. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/syslog_config.py +0 -0
  126. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/tftp_server.py +0 -0
  127. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/threat_prevention.py +0 -0
  128. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/tr069_client.py +0 -0
  129. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/tr069_server.py +0 -0
  130. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/traffic_shaping.py +0 -0
  131. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/upnp_client.py +0 -0
  132. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/vlan_client.py +0 -0
  133. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/wan_link_admin.py +0 -0
  134. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/wifi_bss.py +0 -0
  135. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/wifi_client.py +0 -0
  136. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/wifi_mesh.py +0 -0
  137. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/wifi_onboarding.py +0 -0
  138. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/wifi_radio.py +0 -0
  139. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/wifi_rf.py +0 -0
  140. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/wifi_stations.py +0 -0
  141. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols/wifi_transitions.py +0 -0
  142. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols.egg-info/SOURCES.txt +0 -0
  143. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols.egg-info/dependency_links.txt +0 -0
  144. {testprotocols-0.6.0 → testprotocols-0.7.1}/src/testprotocols.egg-info/top_level.txt +0 -0
  145. {testprotocols-0.6.0 → testprotocols-0.7.1}/tests/test_package_imports.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: testprotocols
3
- Version: 0.6.0
3
+ Version: 0.7.1
4
4
  Summary: Common Test Resource Layer — capability and device protocols for telco testing.
5
5
  Author-email: Alottabits <rjvisser@alottabits.com>
6
6
  Maintainer-email: Alottabits <rjvisser@alottabits.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "testprotocols"
3
- version = "0.6.0"
3
+ version = "0.7.1"
4
4
  description = "Common Test Resource Layer — capability and device protocols for telco testing."
5
5
  requires-python = ">=3.12"
6
6
  dependencies = []
@@ -1,6 +1,6 @@
1
1
  """testprotocols — capability and device Protocols for telco resources under test."""
2
2
 
3
- __version__ = "0.6.0"
3
+ __version__ = "0.7.1"
4
4
 
5
5
  from testprotocols.aftr_gateway import AftrGateway
6
6
  from testprotocols.appliance_nat import ApplianceNat
@@ -18,9 +18,7 @@ from testprotocols.models.switch import BindingSource, FhsBinding, FhsScope, Fhs
18
18
  class FirstHopSecurity(Protocol):
19
19
  """Abstract contract for DHCP snooping + Dynamic ARP Inspection."""
20
20
 
21
- def set_dhcp_snooping(
22
- self, scope: FhsScope, enabled: bool, vlan: int | None = None
23
- ) -> None:
21
+ def set_dhcp_snooping(self, scope: FhsScope, enabled: bool, vlan: int | None = None) -> None:
24
22
  """Enable/disable DHCP snooping globally or per-VLAN. A switch-wide-only
25
23
  product maps ``PER_VLAN`` to ``GLOBAL`` or raises unsupported-capability."""
26
24
  ...
@@ -19,7 +19,6 @@ class IperfClient(Protocol):
19
19
  traffic_port: int,
20
20
  bandwidth: int | None = None,
21
21
  bind_to_ip: str | None = None,
22
- direction: str | None = None,
23
22
  ip_version: int | None = None,
24
23
  udp_protocol: bool = False,
25
24
  time: int = 10,
@@ -51,10 +50,6 @@ class IperfClient(Protocol):
51
50
  ``tcp_rmem``/``tcp_wmem``), so the host must be provisioned
52
51
  accordingly.
53
52
 
54
- ``direction`` is DEPRECATED: a raw CLI fragment appended verbatim
55
- after the typed flags. Kept as an escape hatch only; use the typed
56
- parameters instead.
57
-
58
53
  Returns a tuple of (pid, log_file_path).
59
54
  """
60
55
  ...
@@ -41,7 +41,7 @@ class DhcpLeaseObservation:
41
41
  server: str = ""
42
42
  """The DHCP server identifier, option 54 (dotted quad; empty if absent)."""
43
43
 
44
- options: Mapping[int, str] = field(default_factory=dict)
44
+ options: Mapping[int, str] = field(default_factory=dict[int, str])
45
45
  """Raw received value per option code, as reported by the client."""
46
46
 
47
47
 
@@ -227,7 +227,7 @@ class OneToOneNatRule:
227
227
  public_ip: str
228
228
  lan_ip: str
229
229
  uplink: str = "any"
230
- allowed_inbound: list[NatInboundAllow] = field(default_factory=list)
230
+ allowed_inbound: list[NatInboundAllow] = field(default_factory=list[NatInboundAllow])
231
231
 
232
232
 
233
233
  @dataclass
@@ -236,7 +236,7 @@ class OneToManyNatRule:
236
236
 
237
237
  public_ip: str
238
238
  uplink: str = "any"
239
- port_forwards: list[PortForwardRule] = field(default_factory=list)
239
+ port_forwards: list[PortForwardRule] = field(default_factory=list[PortForwardRule])
240
240
 
241
241
 
242
242
  # --- WAN uplinks ---
@@ -287,7 +287,7 @@ class SyslogServer:
287
287
 
288
288
  host: str
289
289
  port: int = 514
290
- roles: list[SyslogRole] = field(default_factory=list)
290
+ roles: list[SyslogRole] = field(default_factory=list[SyslogRole])
291
291
 
292
292
 
293
293
  # --- Threat prevention (IDS / IPS + malware) ---
@@ -423,10 +423,10 @@ class VlanConfig:
423
423
  appliance_ip: str
424
424
  dhcp_mode: DhcpMode = DhcpMode.SERVER
425
425
  dhcp_lease_seconds: int = 86400
426
- dns_servers: list[str] = field(default_factory=list)
427
- dhcp_options: list[DhcpOption] = field(default_factory=list)
428
- reservations: list[DhcpReservation] = field(default_factory=list)
429
- reserved_ranges: list[tuple[str, str]] = field(default_factory=list)
426
+ dns_servers: list[str] = field(default_factory=list[str])
427
+ dhcp_options: list[DhcpOption] = field(default_factory=list[DhcpOption])
428
+ reservations: list[DhcpReservation] = field(default_factory=list[DhcpReservation])
429
+ reserved_ranges: list[tuple[str, str]] = field(default_factory=list[tuple[str, str]])
430
430
 
431
431
 
432
432
  @dataclass
@@ -489,8 +489,8 @@ class SiteToSiteVpnConfig:
489
489
  """
490
490
 
491
491
  role: VpnRole
492
- hubs: list[VpnHub] = field(default_factory=list)
493
- subnets: list[VpnSubnet] = field(default_factory=list)
492
+ hubs: list[VpnHub] = field(default_factory=list[VpnHub])
493
+ subnets: list[VpnSubnet] = field(default_factory=list[VpnSubnet])
494
494
 
495
495
 
496
496
  @dataclass
@@ -619,8 +619,8 @@ class BgpConfig:
619
619
 
620
620
  enabled: bool
621
621
  as_number: int
622
- neighbors: list[BgpNeighbor] = field(default_factory=list)
623
- advertised_networks: list[str] = field(default_factory=list)
622
+ neighbors: list[BgpNeighbor] = field(default_factory=list[BgpNeighbor])
623
+ advertised_networks: list[str] = field(default_factory=list[str])
624
624
 
625
625
 
626
626
  @dataclass
@@ -145,7 +145,7 @@ class SwitchPort:
145
145
  mode: PortMode
146
146
  enabled: bool = True
147
147
  native_vlan: int | None = None
148
- allowed_vlans: list[int] = field(default_factory=list)
148
+ allowed_vlans: list[int] = field(default_factory=list[int])
149
149
  description: str = ""
150
150
  voice_vlan: int | None = None
151
151
  isolated: bool = False
@@ -195,7 +195,7 @@ class AccessPolicy:
195
195
 
196
196
  port: str
197
197
  policy_type: AccessPolicyType
198
- allowed_macs: list[str] = field(default_factory=list)
198
+ allowed_macs: list[str] = field(default_factory=list[str])
199
199
  max_macs: int | None = None
200
200
  sticky: bool = False
201
201
 
@@ -209,7 +209,7 @@ class StormControlConfig:
209
209
  """
210
210
 
211
211
  port: str
212
- thresholds: dict[StormControlType, float] = field(default_factory=dict)
212
+ thresholds: dict[StormControlType, float] = field(default_factory=dict[StormControlType, float])
213
213
 
214
214
 
215
215
  @dataclass
@@ -81,11 +81,11 @@ class InterfaceDhcpConfig:
81
81
  interface: str
82
82
  mode: DhcpMode = DhcpMode.DISABLED
83
83
  lease_seconds: int = 86400
84
- dns_servers: list[str] = field(default_factory=list)
85
- options: list[DhcpOption] = field(default_factory=list)
86
- reservations: list[DhcpReservation] = field(default_factory=list)
87
- reserved_ranges: list[tuple[str, str]] = field(default_factory=list)
88
- relay_targets: list[str] = field(default_factory=list)
84
+ dns_servers: list[str] = field(default_factory=list[str])
85
+ options: list[DhcpOption] = field(default_factory=list[DhcpOption])
86
+ reservations: list[DhcpReservation] = field(default_factory=list[DhcpReservation])
87
+ reserved_ranges: list[tuple[str, str]] = field(default_factory=list[tuple[str, str]])
88
+ relay_targets: list[str] = field(default_factory=list[str])
89
89
 
90
90
 
91
91
  @dataclass
@@ -105,8 +105,8 @@ class OspfConfig:
105
105
  enabled: bool
106
106
  router_id: str
107
107
  version: OspfVersion = OspfVersion.V2
108
- areas: list[str] = field(default_factory=list)
109
- interfaces: list[OspfInterfaceSettings] = field(default_factory=list)
108
+ areas: list[str] = field(default_factory=list[str])
109
+ interfaces: list[OspfInterfaceSettings] = field(default_factory=list[OspfInterfaceSettings])
110
110
 
111
111
 
112
112
  @dataclass
@@ -5,7 +5,9 @@ policy application, SLA policies, and application-flow visibility.
5
5
 
6
6
  Firewall-rule administration is **not** here — it moved to the dedicated
7
7
  ``l3_firewall`` / ``l7_firewall`` capabilities (coherent-domain split; see
8
- SPLITS.md). Typed path steering (``set_uplink_selection`` /
8
+ SPLITS.md). Also not here: moving flows between the overlay and internet
9
+ domains (overlay membership carve-outs) — see ``site_to_site_vpn``. Typed
10
+ path steering (``set_uplink_selection`` /
9
11
  ``get_uplink_selection`` over ordered ``UplinkSelectionRule``s; performance
10
12
  classes reuse ``SLAPolicy`` by name) landed 2026-06-12; the default
11
13
  (primary) uplink pair (``get_default_uplink`` / ``set_default_uplink``)
@@ -21,8 +21,12 @@ hub anywhere → ``HUB``; overlay enabled with hub list → ``SPOKE``) and on
21
21
  write registers/dereferences the device in the relevant site configs. The
22
22
  round-trip is intent-preserving even where the stored shape differs.
23
23
 
24
- In scope: overlay participation (role, hubs + default route, subnets) and
25
- peer status.
24
+ In scope: overlay participation (role, hubs + default route, subnets),
25
+ overlay exclusions (flow-level membership carve-outs — local breakout), and
26
+ peer status. Overlay *membership* — which flows are in the overlay at all
27
+ (role, hubs + default route, subnets, exclusions) — lives here; path
28
+ selection *within* a domain (e.g. which overlay uplink, or which internet
29
+ uplink) lives in ``sdwan_policy_manager``.
26
30
 
27
31
  Out of scope: VPN-scoped firewall rules (see ``l3_firewall``), IPsec crypto
28
32
  parameters (no driving test; highly vendor-divergent — add on evidence),
@@ -34,7 +38,11 @@ from __future__ import annotations
34
38
 
35
39
  from typing import Protocol, runtime_checkable
36
40
 
37
- from testprotocols.models.sdwan_appliance import SiteToSiteVpnConfig, VpnPeerStatus
41
+ from testprotocols.models.sdwan_appliance import (
42
+ FlowMatch,
43
+ SiteToSiteVpnConfig,
44
+ VpnPeerStatus,
45
+ )
38
46
 
39
47
 
40
48
  @runtime_checkable
@@ -59,3 +67,24 @@ class SiteToSiteVpn(Protocol):
59
67
  Empty list when the device participates in no overlay.
60
68
  """
61
69
  ...
70
+
71
+ def get_vpn_exclusions(self) -> list[FlowMatch]:
72
+ """Return the flow matches excluded from the overlay (local breakout).
73
+
74
+ Flows matching an entry bypass the site-to-site overlay and egress
75
+ the local uplink, even when an overlay route — including an overlay
76
+ default learned from a routing protocol — covers the destination.
77
+ Empty list when every flow is eligible for overlay routing.
78
+ """
79
+ ...
80
+
81
+ def set_vpn_exclusions(self, rules: list[FlowMatch]) -> None:
82
+ """Replace the overlay-exclusion rule set with *rules*.
83
+
84
+ Whole-replace; read-modify-write is the caller's responsibility.
85
+ Products that cannot express a constrained field (e.g. source-side
86
+ matches) raise rather than approximate. Vendor rule kinds outside
87
+ this contract (e.g. application-based exclusions) are preserved
88
+ unchanged by the driver.
89
+ """
90
+ ...
@@ -17,9 +17,7 @@ from testprotocols.models.switch import AclDirection, SwitchAclRule
17
17
  class SwitchAcl(Protocol):
18
18
  """Abstract contract for the switch ACL."""
19
19
 
20
- def set_acl(
21
- self, binding: str, direction: AclDirection, rules: list[SwitchAclRule]
22
- ) -> None:
20
+ def set_acl(self, binding: str, direction: AclDirection, rules: list[SwitchAclRule]) -> None:
23
21
  """Replace the ordered ACL bound to *binding* (a port or ``vlan:<id>``)
24
22
  in *direction*."""
25
23
  ...
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: testprotocols
3
- Version: 0.6.0
3
+ Version: 0.7.1
4
4
  Summary: Common Test Resource Layer — capability and device protocols for telco testing.
5
5
  Author-email: Alottabits <rjvisser@alottabits.com>
6
6
  Maintainer-email: Alottabits <rjvisser@alottabits.com>
@@ -0,0 +1,52 @@
1
+ """Protocol-conformance tests for ApplianceVlans (runtime_checkable structural check)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from testprotocols.appliance_vlans import ApplianceVlans
6
+ from testprotocols.models.sdwan_appliance import DhcpLease, VlanConfig
7
+
8
+
9
+ class _Complete:
10
+ """Has every ApplianceVlans member, including delete_vlan.
11
+
12
+ Signatures mirror the protocol for documentation only — ``runtime_checkable``
13
+ ``isinstance`` checks member presence, not shape, so these bodies are never
14
+ reached.
15
+ """
16
+
17
+ def list_vlans(self) -> list[VlanConfig]:
18
+ raise NotImplementedError
19
+
20
+ def get_vlan(self, vlan_id: int) -> VlanConfig:
21
+ raise NotImplementedError
22
+
23
+ def set_vlan(self, config: VlanConfig) -> None: ...
24
+
25
+ def delete_vlan(self, vlan_id: int) -> None: ...
26
+
27
+ def get_dhcp_leases(self, vlan_id: int | None = None) -> list[DhcpLease]:
28
+ raise NotImplementedError
29
+
30
+
31
+ class _NoDelete:
32
+ """Missing delete_vlan — must NOT satisfy the protocol once delete_vlan is declared."""
33
+
34
+ def list_vlans(self) -> list[VlanConfig]:
35
+ raise NotImplementedError
36
+
37
+ def get_vlan(self, vlan_id: int) -> VlanConfig:
38
+ raise NotImplementedError
39
+
40
+ def set_vlan(self, config: VlanConfig) -> None: ...
41
+
42
+ def get_dhcp_leases(self, vlan_id: int | None = None) -> list[DhcpLease]:
43
+ raise NotImplementedError
44
+
45
+
46
+ def test_complete_impl_satisfies_protocol() -> None:
47
+ assert isinstance(_Complete(), ApplianceVlans)
48
+
49
+
50
+ def test_delete_vlan_is_required_by_protocol() -> None:
51
+ # runtime_checkable isinstance requires every declared member be present.
52
+ assert not isinstance(_NoDelete(), ApplianceVlans)
@@ -9,6 +9,7 @@ from __future__ import annotations
9
9
  import importlib
10
10
 
11
11
  import pytest
12
+ from _helpers import protocol_attrs
12
13
 
13
14
  PROTOCOLS = [
14
15
  (
@@ -93,5 +94,5 @@ def test_is_runtime_checkable(class_name: str, module: str, expected_methods: se
93
94
  def test_protocol_shape(class_name: str, module: str, expected_methods: set[str]) -> None:
94
95
  """Each Protocol declares at least the expected method set."""
95
96
  cls = getattr(importlib.import_module(module), class_name)
96
- actual = set(cls.__protocol_attrs__)
97
+ actual = protocol_attrs(cls)
97
98
  assert expected_methods <= actual, f"{class_name} missing: {expected_methods - actual}"
@@ -10,6 +10,7 @@ inventory-string -> Protocol binding and the per-archetype capability set
10
10
  from __future__ import annotations
11
11
 
12
12
  import pytest
13
+ from _helpers import protocol_attrs
13
14
  from testprotocols.devices import DeviceTypeSpec, all_device_types, get_device_type
14
15
  from testprotocols.devices.base import BaseDeviceProtocol
15
16
  from testprotocols.devices.client import (
@@ -195,7 +196,7 @@ def test_sdwan_router_aggregates_expected_capabilities() -> None:
195
196
  "nat",
196
197
  "conntrack",
197
198
  }
198
- actual = set(SdwanRouterDevice.__protocol_attrs__)
199
+ actual = protocol_attrs(SdwanRouterDevice)
199
200
  assert expected <= actual, f"missing: {expected - actual}"
200
201
  assert "netem" not in actual, "netem should not be on SdwanRouterDevice"
201
202
 
@@ -220,7 +221,7 @@ def test_sdwan_appliance_aggregates_expected_capabilities() -> None:
220
221
  "syslog",
221
222
  "vpn",
222
223
  }
223
- actual = set(SdwanApplianceDevice.__protocol_attrs__)
224
+ actual = protocol_attrs(SdwanApplianceDevice)
224
225
  assert expected <= actual, f"missing: {expected - actual}"
225
226
  # device_management is Linux-host-shaped (ps/memory/board-logs/file-content)
226
227
  # and intentionally NOT on the managed-appliance archetype — see GAPS.md.
@@ -257,7 +258,7 @@ def test_cpe_aggregates_expected_capabilities() -> None:
257
258
  "firewall_zones",
258
259
  "ntp_client",
259
260
  }
260
- actual = set(CpeDevice.__protocol_attrs__)
261
+ actual = protocol_attrs(CpeDevice)
261
262
  assert expected <= actual, f"missing: {expected - actual}"
262
263
 
263
264
 
@@ -281,7 +282,7 @@ def test_lan_client_aggregates_expected_capabilities() -> None:
281
282
  "arp_client",
282
283
  "vlan_client",
283
284
  }
284
- actual = set(LanClientDevice.__protocol_attrs__)
285
+ actual = protocol_attrs(LanClientDevice)
285
286
  assert expected <= actual, f"missing: {expected - actual}"
286
287
 
287
288
 
@@ -301,13 +302,13 @@ def test_wlan_client_aggregates_expected_capabilities() -> None:
301
302
  "nmap_scanner",
302
303
  "file_transfer",
303
304
  }
304
- actual = set(WlanClientDevice.__protocol_attrs__)
305
+ actual = protocol_attrs(WlanClientDevice)
305
306
  assert expected <= actual, f"missing: {expected - actual}"
306
307
 
307
308
 
308
309
  def test_qoe_client_aggregates_expected_capabilities() -> None:
309
310
  expected = {"qoe_browser", "ip_interface", "dhcp_client", "syslog"}
310
- actual = set(QoeClientDevice.__protocol_attrs__)
311
+ actual = protocol_attrs(QoeClientDevice)
311
312
  assert expected <= actual, f"missing: {expected - actual}"
312
313
 
313
314
 
@@ -323,15 +324,13 @@ def test_qoe_measurement_client_aggregates_expected_capabilities() -> None:
323
324
  "pcap",
324
325
  "syslog",
325
326
  }
326
- actual = set(QoeMeasurementClientDevice.__protocol_attrs__)
327
+ actual = protocol_attrs(QoeMeasurementClientDevice)
327
328
  assert expected <= actual, f"missing: {expected - actual}"
328
329
 
329
330
 
330
331
  def test_qoe_measurement_client_is_a_qoe_client() -> None:
331
332
  """Strict-superset invariant: the measurement client subsumes QoeClientDevice."""
332
- assert set(QoeClientDevice.__protocol_attrs__) <= set(
333
- QoeMeasurementClientDevice.__protocol_attrs__
334
- )
333
+ assert protocol_attrs(QoeClientDevice) <= protocol_attrs(QoeMeasurementClientDevice)
335
334
 
336
335
 
337
336
  def test_acs_aggregates_expected_capabilities() -> None:
@@ -341,43 +340,43 @@ def test_acs_aggregates_expected_capabilities() -> None:
341
340
  "file_transfer",
342
341
  "packet_filter",
343
342
  }
344
- actual = set(AcsDevice.__protocol_attrs__)
343
+ actual = protocol_attrs(AcsDevice)
345
344
  assert expected <= actual, f"missing: {expected - actual}"
346
345
 
347
346
 
348
347
  def test_provisioner_aggregates_expected_capabilities() -> None:
349
348
  expected = {"dhcp_server", "pcap", "file_transfer", "packet_filter"}
350
- actual = set(ProvisionerDevice.__protocol_attrs__)
349
+ actual = protocol_attrs(ProvisionerDevice)
351
350
  assert expected <= actual, f"missing: {expected - actual}"
352
351
 
353
352
 
354
353
  def test_tftp_aggregates_expected_capabilities() -> None:
355
354
  expected = {"tftp_server"}
356
- actual = set(TftpDevice.__protocol_attrs__)
355
+ actual = protocol_attrs(TftpDevice)
357
356
  assert expected <= actual, f"missing: {expected - actual}"
358
357
 
359
358
 
360
359
  def test_traffic_controller_aggregates_expected_capabilities() -> None:
361
360
  expected = {"netem", "ip_interface", "pcap", "syslog"}
362
- actual = set(TrafficControllerDevice.__protocol_attrs__)
361
+ actual = protocol_attrs(TrafficControllerDevice)
363
362
  assert expected <= actual, f"missing: {expected - actual}"
364
363
 
365
364
 
366
365
  def test_iperf_traffic_generator_aggregates_expected_capabilities() -> None:
367
366
  expected = {"iperf_generator", "ip_interface"}
368
- actual = set(IperfTrafficGeneratorDevice.__protocol_attrs__)
367
+ actual = protocol_attrs(IperfTrafficGeneratorDevice)
369
368
  assert expected <= actual, f"missing: {expected - actual}"
370
369
 
371
370
 
372
371
  def test_sip_phone_device_aggregates_expected_capabilities() -> None:
373
372
  expected = {"sip_phone", "ip_interface", "dhcp_client", "number"}
374
- actual = set(SipPhoneDevice.__protocol_attrs__)
373
+ actual = protocol_attrs(SipPhoneDevice)
375
374
  assert expected <= actual, f"missing: {expected - actual}"
376
375
 
377
376
 
378
377
  def test_sip_server_device_aggregates_expected_capabilities() -> None:
379
378
  expected = {"sip_server", "pcap", "file_transfer"}
380
- actual = set(SipServerDevice.__protocol_attrs__)
379
+ actual = protocol_attrs(SipServerDevice)
381
380
  assert expected <= actual, f"missing: {expected - actual}"
382
381
 
383
382
 
@@ -400,7 +399,7 @@ def test_wan_server_aggregates_expected_capabilities() -> None:
400
399
  "nat",
401
400
  "conntrack",
402
401
  }
403
- actual = set(WanServerDevice.__protocol_attrs__)
402
+ actual = protocol_attrs(WanServerDevice)
404
403
  assert expected <= actual, f"missing: {expected - actual}"
405
404
 
406
405
 
@@ -411,7 +410,7 @@ def test_wan_server_aggregates_expected_capabilities() -> None:
411
410
 
412
411
  def test_acs_firewall_facets() -> None:
413
412
  """ACS aggregates packet_filter only — no NAT / port-forwarding / conntrack / zones."""
414
- attrs = set(AcsDevice.__protocol_attrs__)
413
+ attrs = protocol_attrs(AcsDevice)
415
414
  assert "packet_filter" in attrs
416
415
  for absent in ("nat", "port_forwarding", "conntrack", "firewall_zones"):
417
416
  assert absent not in attrs, f"AcsDevice unexpectedly aggregates {absent}"
@@ -419,13 +418,13 @@ def test_acs_firewall_facets() -> None:
419
418
 
420
419
  def test_provisioner_firewall_facets() -> None:
421
420
  """Provisioner aggregates packet_filter."""
422
- attrs = set(ProvisionerDevice.__protocol_attrs__)
421
+ attrs = protocol_attrs(ProvisionerDevice)
423
422
  assert "packet_filter" in attrs
424
423
 
425
424
 
426
425
  def test_lan_client_firewall_facets() -> None:
427
426
  """LanClientDevice aggregates packet_filter only."""
428
- attrs = set(LanClientDevice.__protocol_attrs__)
427
+ attrs = protocol_attrs(LanClientDevice)
429
428
  assert "packet_filter" in attrs
430
429
  for absent in ("nat", "port_forwarding", "conntrack", "firewall_zones"):
431
430
  assert absent not in attrs, f"LanClientDevice unexpectedly aggregates {absent}"
@@ -433,7 +432,7 @@ def test_lan_client_firewall_facets() -> None:
433
432
 
434
433
  def test_wan_server_firewall_facets() -> None:
435
434
  """WanServerDevice aggregates packet_filter, nat, conntrack — no port-forwarding / zones."""
436
- attrs = set(WanServerDevice.__protocol_attrs__)
435
+ attrs = protocol_attrs(WanServerDevice)
437
436
  for present in ("packet_filter", "nat", "conntrack"):
438
437
  assert present in attrs
439
438
  for absent in ("port_forwarding", "firewall_zones"):
@@ -442,7 +441,7 @@ def test_wan_server_firewall_facets() -> None:
442
441
 
443
442
  def test_cpe_full_firewall_surface() -> None:
444
443
  """CpeDevice aggregates the complete firewall surface."""
445
- attrs = set(CpeDevice.__protocol_attrs__)
444
+ attrs = protocol_attrs(CpeDevice)
446
445
  for present in ("firewall", "nat", "conntrack", "firewall_zones"):
447
446
  assert present in attrs
448
447
  # PortForwarding is no longer a separate attribute — it's bundled into firewall.
@@ -455,7 +454,12 @@ def test_cpe_full_firewall_surface() -> None:
455
454
  def test_devices_without_firewall() -> None:
456
455
  """These archetypes must not pull in any of the firewall capabilities."""
457
456
  firewall_attrs = (
458
- "packet_filter", "firewall", "nat", "port_forwarding", "conntrack", "firewall_zones"
457
+ "packet_filter",
458
+ "firewall",
459
+ "nat",
460
+ "port_forwarding",
461
+ "conntrack",
462
+ "firewall_zones",
459
463
  )
460
464
  for archetype in (
461
465
  TftpDevice,
@@ -466,7 +470,7 @@ def test_devices_without_firewall() -> None:
466
470
  TrafficControllerDevice,
467
471
  IperfTrafficGeneratorDevice,
468
472
  ):
469
- attrs = set(archetype.__protocol_attrs__)
473
+ attrs = protocol_attrs(archetype)
470
474
  for absent in firewall_attrs:
471
475
  assert absent not in attrs, f"{archetype.__name__} unexpectedly aggregates {absent}"
472
476
 
@@ -497,10 +501,10 @@ def test_archetype_inherits_base_device_protocol(archetype: type) -> None:
497
501
  @pytest.mark.parametrize("archetype", _ALL_ARCHETYPES, ids=lambda a: a.__name__)
498
502
  def test_archetype_carries_universal_identity(archetype: type) -> None:
499
503
  """Every archetype's protocol-attrs include device_name and device_type."""
500
- attrs = set(archetype.__protocol_attrs__)
504
+ attrs = protocol_attrs(archetype)
501
505
  assert {"device_name", "device_type"} <= attrs, (
502
506
  f"{archetype.__name__} missing universal-identity attrs: "
503
- f"{ {'device_name', 'device_type'} - attrs}"
507
+ f"{ {'device_name', 'device_type'} - attrs }"
504
508
  )
505
509
 
506
510
 
@@ -529,36 +533,57 @@ def test_l3_switch_routed_registered() -> None:
529
533
 
530
534
  def test_l3_switch_is_strict_superset_of_l2() -> None:
531
535
  # Protocol inheritance: every L3 attribute set includes the full L2 set.
532
- l2_attrs = set(L2Switch.__protocol_attrs__)
533
- l3_attrs = set(L3Switch.__protocol_attrs__)
536
+ l2_attrs = protocol_attrs(L2Switch)
537
+ l3_attrs = protocol_attrs(L3Switch)
534
538
  assert l2_attrs <= l3_attrs, f"L3Switch missing L2 attrs: {l2_attrs - l3_attrs}"
535
539
  expected_l3 = {
536
- "routed_interfaces", "static_routes", "routing_read",
537
- "ospf", "interface_dhcp", "gateway_redundancy",
540
+ "routed_interfaces",
541
+ "static_routes",
542
+ "routing_read",
543
+ "ospf",
544
+ "interface_dhcp",
545
+ "gateway_redundancy",
538
546
  }
539
547
  assert expected_l3 <= l3_attrs, f"missing: {expected_l3 - l3_attrs}"
540
548
  assert "bgp" not in l3_attrs, "bgp belongs only on L3SwitchRouted"
541
549
 
542
550
 
543
551
  def test_l3_switch_routed_adds_bgp() -> None:
544
- attrs = set(L3SwitchRouted.__protocol_attrs__)
552
+ attrs = protocol_attrs(L3SwitchRouted)
545
553
  assert "bgp" in attrs
546
- assert set(L3Switch.__protocol_attrs__) <= attrs # still a superset of L3Switch
554
+ assert protocol_attrs(L3Switch) <= attrs # still a superset of L3Switch
547
555
 
548
556
 
549
557
  def test_l2_switch_aggregates_expected_capabilities() -> None:
550
558
  expected = {
551
- "switch_ports", "switch_vlans", "spanning_tree", "link_aggregation",
552
- "port_poe", "port_security", "radius", "first_hop_security",
553
- "storm_control", "switch_acl", "discovery", "mac_table",
554
- "port_status", "switch_qos", "syslog", "ntp",
559
+ "switch_ports",
560
+ "switch_vlans",
561
+ "spanning_tree",
562
+ "link_aggregation",
563
+ "port_poe",
564
+ "port_security",
565
+ "radius",
566
+ "first_hop_security",
567
+ "storm_control",
568
+ "switch_acl",
569
+ "discovery",
570
+ "mac_table",
571
+ "port_status",
572
+ "switch_qos",
573
+ "syslog",
574
+ "ntp",
555
575
  }
556
- actual = set(L2Switch.__protocol_attrs__)
576
+ actual = protocol_attrs(L2Switch)
557
577
  assert expected <= actual, f"missing: {expected - actual}"
558
578
  # host-substrate levers stay on the Linux twins, never on a managed switch
559
579
  for host_lever in (
560
- "conntrack", "pcap", "ip_interface", "nat",
561
- "packet_filter", "firewall_zones", "wan_admin",
580
+ "conntrack",
581
+ "pcap",
582
+ "ip_interface",
583
+ "nat",
584
+ "packet_filter",
585
+ "firewall_zones",
586
+ "wan_admin",
562
587
  ):
563
588
  assert host_lever not in actual, f"{host_lever} must not be on L2Switch"
564
589
 
@@ -570,19 +595,29 @@ def test_l3_switch_excludes_host_substrate_levers() -> None:
570
595
  Linux-host-shaped ``ip_interface``; firewall / NAT / conntrack belong on the
571
596
  CPE/router archetypes; wan_admin is a Linux-only forced link-down lever.
572
597
  """
573
- actual = set(L3Switch.__protocol_attrs__)
598
+ actual = protocol_attrs(L3Switch)
574
599
  for host_lever in (
575
- "conntrack", "pcap", "ip_interface", "nat",
576
- "packet_filter", "firewall_zones", "wan_admin",
600
+ "conntrack",
601
+ "pcap",
602
+ "ip_interface",
603
+ "nat",
604
+ "packet_filter",
605
+ "firewall_zones",
606
+ "wan_admin",
577
607
  ):
578
608
  assert host_lever not in actual, f"{host_lever} must not be on L3Switch"
579
609
 
580
610
 
581
611
  def test_l3_switch_routed_excludes_host_substrate_levers() -> None:
582
612
  """L3SwitchRouted (the BGP-capable variant) likewise excludes host-substrate levers."""
583
- actual = set(L3SwitchRouted.__protocol_attrs__)
613
+ actual = protocol_attrs(L3SwitchRouted)
584
614
  for host_lever in (
585
- "conntrack", "pcap", "ip_interface", "nat",
586
- "packet_filter", "firewall_zones", "wan_admin",
615
+ "conntrack",
616
+ "pcap",
617
+ "ip_interface",
618
+ "nat",
619
+ "packet_filter",
620
+ "firewall_zones",
621
+ "wan_admin",
587
622
  ):
588
623
  assert host_lever not in actual, f"{host_lever} must not be on L3SwitchRouted"