testprotocols 0.7.0__tar.gz → 0.7.2__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.7.0/src/testprotocols.egg-info → testprotocols-0.7.2}/PKG-INFO +1 -1
  2. {testprotocols-0.7.0 → testprotocols-0.7.2}/pyproject.toml +1 -1
  3. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/__init__.py +1 -1
  4. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/devices/client.py +18 -0
  5. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/devices/sdwan.py +6 -0
  6. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/devices/switch.py +6 -0
  7. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/first_hop_security.py +1 -3
  8. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/models/dhcp.py +1 -1
  9. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/models/sdwan_appliance.py +11 -11
  10. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/models/switch.py +3 -3
  11. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/models/switch_routing.py +7 -7
  12. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/switch_acl.py +1 -3
  13. {testprotocols-0.7.0 → testprotocols-0.7.2/src/testprotocols.egg-info}/PKG-INFO +1 -1
  14. testprotocols-0.7.2/tests/test_appliance_vlans.py +52 -0
  15. {testprotocols-0.7.0 → testprotocols-0.7.2}/tests/test_cpe_templates.py +2 -1
  16. {testprotocols-0.7.0 → testprotocols-0.7.2}/tests/test_device_types.py +87 -46
  17. {testprotocols-0.7.0 → testprotocols-0.7.2}/tests/test_dhcp_templates.py +3 -2
  18. {testprotocols-0.7.0 → testprotocols-0.7.2}/tests/test_firewall_templates.py +5 -4
  19. {testprotocols-0.7.0 → testprotocols-0.7.2}/tests/test_infra_templates.py +2 -1
  20. {testprotocols-0.7.0 → testprotocols-0.7.2}/tests/test_ip_interface.py +2 -1
  21. {testprotocols-0.7.0 → testprotocols-0.7.2}/tests/test_ip_routing.py +2 -1
  22. {testprotocols-0.7.0 → testprotocols-0.7.2}/tests/test_network_tool_templates.py +2 -1
  23. {testprotocols-0.7.0 → testprotocols-0.7.2}/tests/test_radius_templates.py +2 -1
  24. {testprotocols-0.7.0 → testprotocols-0.7.2}/tests/test_sdwan_appliance_models.py +16 -13
  25. {testprotocols-0.7.0 → testprotocols-0.7.2}/tests/test_sdwan_appliance_templates.py +2 -1
  26. {testprotocols-0.7.0 → testprotocols-0.7.2}/tests/test_switch_models.py +19 -18
  27. {testprotocols-0.7.0 → testprotocols-0.7.2}/tests/test_switch_routing_models.py +12 -7
  28. testprotocols-0.7.2/tests/test_switch_templates.py +95 -0
  29. {testprotocols-0.7.0 → testprotocols-0.7.2}/tests/test_traffic_templates.py +2 -1
  30. {testprotocols-0.7.0 → testprotocols-0.7.2}/tests/test_voice_wifi_templates.py +2 -1
  31. {testprotocols-0.7.0 → testprotocols-0.7.2}/tests/test_wan_edge_templates.py +4 -3
  32. {testprotocols-0.7.0 → testprotocols-0.7.2}/tests/test_wifi_templates.py +6 -5
  33. testprotocols-0.7.0/tests/test_appliance_vlans.py +0 -33
  34. testprotocols-0.7.0/tests/test_switch_templates.py +0 -69
  35. {testprotocols-0.7.0 → testprotocols-0.7.2}/LICENSE +0 -0
  36. {testprotocols-0.7.0 → testprotocols-0.7.2}/NOTICE +0 -0
  37. {testprotocols-0.7.0 → testprotocols-0.7.2}/README.md +0 -0
  38. {testprotocols-0.7.0 → testprotocols-0.7.2}/setup.cfg +0 -0
  39. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/aftr_gateway.py +0 -0
  40. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/appliance_nat.py +0 -0
  41. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/appliance_uplinks.py +0 -0
  42. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/appliance_vlans.py +0 -0
  43. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/arp_client.py +0 -0
  44. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/bgp.py +0 -0
  45. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/conntrack.py +0 -0
  46. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/content_filtering.py +0 -0
  47. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/device_lifecycle.py +0 -0
  48. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/device_management.py +0 -0
  49. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/devices/__init__.py +0 -0
  50. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/devices/base.py +0 -0
  51. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/devices/cpe.py +0 -0
  52. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/devices/infra.py +0 -0
  53. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/devices/traffic.py +0 -0
  54. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/devices/voice.py +0 -0
  55. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/devices/wan.py +0 -0
  56. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/dhcp_client.py +0 -0
  57. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/dhcp_server.py +0 -0
  58. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/discovery.py +0 -0
  59. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/dns_client.py +0 -0
  60. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/file_transfer.py +0 -0
  61. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/firewall.py +0 -0
  62. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/firewall_zones.py +0 -0
  63. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/gateway_redundancy.py +0 -0
  64. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/http_client.py +0 -0
  65. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/http_server.py +0 -0
  66. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/hw_console.py +0 -0
  67. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/interface_dhcp.py +0 -0
  68. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/ip_interface.py +0 -0
  69. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/ip_routing.py +0 -0
  70. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/iperf_client.py +0 -0
  71. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/iperf_generator.py +0 -0
  72. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/iperf_server.py +0 -0
  73. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/l3_firewall.py +0 -0
  74. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/l7_firewall.py +0 -0
  75. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/link_aggregation.py +0 -0
  76. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/mac_table.py +0 -0
  77. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/models/__init__.py +1 -1
  78. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/models/firewall.py +0 -0
  79. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/models/impairment.py +0 -0
  80. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/models/l2_common.py +0 -0
  81. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/models/multicast.py +0 -0
  82. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/models/networking.py +0 -0
  83. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/models/packets.py +0 -0
  84. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/models/qoe.py +0 -0
  85. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/models/radius.py +0 -0
  86. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/models/tr069.py +0 -0
  87. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/models/traffic.py +0 -0
  88. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/models/wan_edge.py +0 -0
  89. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/models/wifi.py +0 -0
  90. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/multicast_client.py +0 -0
  91. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/nat.py +0 -0
  92. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/netem_controller.py +0 -0
  93. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/network_endpoint.py +0 -0
  94. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/network_probe.py +0 -0
  95. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/nmap_scanner.py +0 -0
  96. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/ntp_client.py +0 -0
  97. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/ntp_config.py +0 -0
  98. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/ospf.py +0 -0
  99. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/packet_filter.py +0 -0
  100. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/pcap_capture.py +0 -0
  101. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/pdu_controller.py +0 -0
  102. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/port_poe.py +0 -0
  103. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/port_security.py +0 -0
  104. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/port_status.py +0 -0
  105. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/py.typed +0 -0
  106. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/qoe_browser.py +0 -0
  107. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/radius_client.py +0 -0
  108. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/radius_server.py +0 -0
  109. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/reachability_responder.py +0 -0
  110. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/routed_interfaces.py +0 -0
  111. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/router.py +0 -0
  112. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/routing_read.py +0 -0
  113. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/sdwan_policy_manager.py +0 -0
  114. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/sip_phone.py +0 -0
  115. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/sip_server.py +0 -0
  116. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/site_to_site_vpn.py +0 -0
  117. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/snmp_client.py +0 -0
  118. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/spanning_tree.py +0 -0
  119. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/static_routes.py +0 -0
  120. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/storm_control.py +0 -0
  121. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/streaming_server.py +0 -0
  122. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/switch_ports.py +0 -0
  123. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/switch_qos.py +0 -0
  124. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/switch_vlans.py +0 -0
  125. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/syslog_config.py +0 -0
  126. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/tftp_server.py +0 -0
  127. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/threat_prevention.py +0 -0
  128. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/tr069_client.py +0 -0
  129. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/tr069_server.py +0 -0
  130. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/traffic_shaping.py +0 -0
  131. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/upnp_client.py +0 -0
  132. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/vlan_client.py +0 -0
  133. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/wan_link_admin.py +0 -0
  134. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/wifi_bss.py +0 -0
  135. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/wifi_client.py +0 -0
  136. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/wifi_mesh.py +0 -0
  137. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/wifi_onboarding.py +0 -0
  138. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/wifi_radio.py +0 -0
  139. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/wifi_rf.py +0 -0
  140. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/wifi_stations.py +0 -0
  141. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols/wifi_transitions.py +0 -0
  142. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols.egg-info/SOURCES.txt +0 -0
  143. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols.egg-info/dependency_links.txt +0 -0
  144. {testprotocols-0.7.0 → testprotocols-0.7.2}/src/testprotocols.egg-info/top_level.txt +0 -0
  145. {testprotocols-0.7.0 → testprotocols-0.7.2}/tests/test_package_imports.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: testprotocols
3
- Version: 0.7.0
3
+ Version: 0.7.2
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.7.0"
3
+ version = "0.7.2"
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.2"
4
4
 
5
5
  from testprotocols.aftr_gateway import AftrGateway
6
6
  from testprotocols.appliance_nat import ApplianceNat
@@ -142,6 +142,24 @@ class QoeMeasurementClientDevice(QoeClientDevice, Protocol):
142
142
  declaration when address facts are foreign-owned or discovered late.
143
143
  Vendor-neutral metadata from the driver's attachment config."""
144
144
 
145
+ attachment_subnet: str
146
+ """The declared subnet (CIDR) of a guest attachment, empty when undeclared.
147
+ Vendor-neutral metadata from the driver's attachment config — read it here
148
+ rather than reaching into a framework-specific config object."""
149
+
150
+ mgmt_ip: str
151
+ """The client's management-plane address, empty when undeclared. Published
152
+ by the driver from its own config so isolation checks never consume a
153
+ framework-specific config object."""
154
+
155
+ pipe_switch: str
156
+ """Name of the switch carrying this client's dedicated access port (its
157
+ placement pipe), empty when the client has no declared pipe."""
158
+
159
+ pipe_port: str
160
+ """The dedicated access port whose VLAN is this client's placement knob,
161
+ empty when the client has no declared pipe."""
162
+
145
163
 
146
164
  register_device_type("linux_lan_client", LanClientDevice)
147
165
  register_device_type("linux_wlan_client", WlanClientDevice)
@@ -99,5 +99,11 @@ class SdwanApplianceDevice(BaseDeviceProtocol, Protocol):
99
99
  model-parameterised tests; read it here rather than reaching into a
100
100
  framework-specific config object."""
101
101
 
102
+ manages_network: bool
103
+ """Whether this session owns the device's network configuration. False for
104
+ a warm-spare member whose primary owns the writes. Vendor-neutral metadata
105
+ from the driver's config — read it here rather than reaching into a
106
+ framework-specific config object."""
107
+
102
108
 
103
109
  register_device_type("sdwan_appliance", SdwanApplianceDevice)
@@ -68,6 +68,12 @@ class L2Switch(BaseDeviceProtocol, Protocol):
68
68
  syslog: SyslogConfig
69
69
  ntp: NtpConfig
70
70
 
71
+ placement_ports: tuple[str, ...]
72
+ """Designated ports a testbed may re-VLAN to place a client; empty means
73
+ no placement write is permitted on this switch. Independent of which
74
+ device's pipe enters a port: pipe ownership is topology, this is write
75
+ authorization — declared by the switch, the resource's owner."""
76
+
71
77
 
72
78
  register_device_type("managed_switch_l2", L2Switch)
73
79
 
@@ -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
  ...
@@ -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
@@ -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.7.0
3
+ Version: 0.7.2
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
 
@@ -219,8 +220,9 @@ def test_sdwan_appliance_aggregates_expected_capabilities() -> None:
219
220
  "lan",
220
221
  "syslog",
221
222
  "vpn",
223
+ "manages_network",
222
224
  }
223
- actual = set(SdwanApplianceDevice.__protocol_attrs__)
225
+ actual = protocol_attrs(SdwanApplianceDevice)
224
226
  assert expected <= actual, f"missing: {expected - actual}"
225
227
  # device_management is Linux-host-shaped (ps/memory/board-logs/file-content)
226
228
  # and intentionally NOT on the managed-appliance archetype — see GAPS.md.
@@ -257,7 +259,7 @@ def test_cpe_aggregates_expected_capabilities() -> None:
257
259
  "firewall_zones",
258
260
  "ntp_client",
259
261
  }
260
- actual = set(CpeDevice.__protocol_attrs__)
262
+ actual = protocol_attrs(CpeDevice)
261
263
  assert expected <= actual, f"missing: {expected - actual}"
262
264
 
263
265
 
@@ -281,7 +283,7 @@ def test_lan_client_aggregates_expected_capabilities() -> None:
281
283
  "arp_client",
282
284
  "vlan_client",
283
285
  }
284
- actual = set(LanClientDevice.__protocol_attrs__)
286
+ actual = protocol_attrs(LanClientDevice)
285
287
  assert expected <= actual, f"missing: {expected - actual}"
286
288
 
287
289
 
@@ -301,13 +303,13 @@ def test_wlan_client_aggregates_expected_capabilities() -> None:
301
303
  "nmap_scanner",
302
304
  "file_transfer",
303
305
  }
304
- actual = set(WlanClientDevice.__protocol_attrs__)
306
+ actual = protocol_attrs(WlanClientDevice)
305
307
  assert expected <= actual, f"missing: {expected - actual}"
306
308
 
307
309
 
308
310
  def test_qoe_client_aggregates_expected_capabilities() -> None:
309
311
  expected = {"qoe_browser", "ip_interface", "dhcp_client", "syslog"}
310
- actual = set(QoeClientDevice.__protocol_attrs__)
312
+ actual = protocol_attrs(QoeClientDevice)
311
313
  assert expected <= actual, f"missing: {expected - actual}"
312
314
 
313
315
 
@@ -322,16 +324,18 @@ def test_qoe_measurement_client_aggregates_expected_capabilities() -> None:
322
324
  "responder",
323
325
  "pcap",
324
326
  "syslog",
327
+ "attachment_subnet",
328
+ "mgmt_ip",
329
+ "pipe_switch",
330
+ "pipe_port",
325
331
  }
326
- actual = set(QoeMeasurementClientDevice.__protocol_attrs__)
332
+ actual = protocol_attrs(QoeMeasurementClientDevice)
327
333
  assert expected <= actual, f"missing: {expected - actual}"
328
334
 
329
335
 
330
336
  def test_qoe_measurement_client_is_a_qoe_client() -> None:
331
337
  """Strict-superset invariant: the measurement client subsumes QoeClientDevice."""
332
- assert set(QoeClientDevice.__protocol_attrs__) <= set(
333
- QoeMeasurementClientDevice.__protocol_attrs__
334
- )
338
+ assert protocol_attrs(QoeClientDevice) <= protocol_attrs(QoeMeasurementClientDevice)
335
339
 
336
340
 
337
341
  def test_acs_aggregates_expected_capabilities() -> None:
@@ -341,43 +345,43 @@ def test_acs_aggregates_expected_capabilities() -> None:
341
345
  "file_transfer",
342
346
  "packet_filter",
343
347
  }
344
- actual = set(AcsDevice.__protocol_attrs__)
348
+ actual = protocol_attrs(AcsDevice)
345
349
  assert expected <= actual, f"missing: {expected - actual}"
346
350
 
347
351
 
348
352
  def test_provisioner_aggregates_expected_capabilities() -> None:
349
353
  expected = {"dhcp_server", "pcap", "file_transfer", "packet_filter"}
350
- actual = set(ProvisionerDevice.__protocol_attrs__)
354
+ actual = protocol_attrs(ProvisionerDevice)
351
355
  assert expected <= actual, f"missing: {expected - actual}"
352
356
 
353
357
 
354
358
  def test_tftp_aggregates_expected_capabilities() -> None:
355
359
  expected = {"tftp_server"}
356
- actual = set(TftpDevice.__protocol_attrs__)
360
+ actual = protocol_attrs(TftpDevice)
357
361
  assert expected <= actual, f"missing: {expected - actual}"
358
362
 
359
363
 
360
364
  def test_traffic_controller_aggregates_expected_capabilities() -> None:
361
365
  expected = {"netem", "ip_interface", "pcap", "syslog"}
362
- actual = set(TrafficControllerDevice.__protocol_attrs__)
366
+ actual = protocol_attrs(TrafficControllerDevice)
363
367
  assert expected <= actual, f"missing: {expected - actual}"
364
368
 
365
369
 
366
370
  def test_iperf_traffic_generator_aggregates_expected_capabilities() -> None:
367
371
  expected = {"iperf_generator", "ip_interface"}
368
- actual = set(IperfTrafficGeneratorDevice.__protocol_attrs__)
372
+ actual = protocol_attrs(IperfTrafficGeneratorDevice)
369
373
  assert expected <= actual, f"missing: {expected - actual}"
370
374
 
371
375
 
372
376
  def test_sip_phone_device_aggregates_expected_capabilities() -> None:
373
377
  expected = {"sip_phone", "ip_interface", "dhcp_client", "number"}
374
- actual = set(SipPhoneDevice.__protocol_attrs__)
378
+ actual = protocol_attrs(SipPhoneDevice)
375
379
  assert expected <= actual, f"missing: {expected - actual}"
376
380
 
377
381
 
378
382
  def test_sip_server_device_aggregates_expected_capabilities() -> None:
379
383
  expected = {"sip_server", "pcap", "file_transfer"}
380
- actual = set(SipServerDevice.__protocol_attrs__)
384
+ actual = protocol_attrs(SipServerDevice)
381
385
  assert expected <= actual, f"missing: {expected - actual}"
382
386
 
383
387
 
@@ -400,7 +404,7 @@ def test_wan_server_aggregates_expected_capabilities() -> None:
400
404
  "nat",
401
405
  "conntrack",
402
406
  }
403
- actual = set(WanServerDevice.__protocol_attrs__)
407
+ actual = protocol_attrs(WanServerDevice)
404
408
  assert expected <= actual, f"missing: {expected - actual}"
405
409
 
406
410
 
@@ -411,7 +415,7 @@ def test_wan_server_aggregates_expected_capabilities() -> None:
411
415
 
412
416
  def test_acs_firewall_facets() -> None:
413
417
  """ACS aggregates packet_filter only — no NAT / port-forwarding / conntrack / zones."""
414
- attrs = set(AcsDevice.__protocol_attrs__)
418
+ attrs = protocol_attrs(AcsDevice)
415
419
  assert "packet_filter" in attrs
416
420
  for absent in ("nat", "port_forwarding", "conntrack", "firewall_zones"):
417
421
  assert absent not in attrs, f"AcsDevice unexpectedly aggregates {absent}"
@@ -419,13 +423,13 @@ def test_acs_firewall_facets() -> None:
419
423
 
420
424
  def test_provisioner_firewall_facets() -> None:
421
425
  """Provisioner aggregates packet_filter."""
422
- attrs = set(ProvisionerDevice.__protocol_attrs__)
426
+ attrs = protocol_attrs(ProvisionerDevice)
423
427
  assert "packet_filter" in attrs
424
428
 
425
429
 
426
430
  def test_lan_client_firewall_facets() -> None:
427
431
  """LanClientDevice aggregates packet_filter only."""
428
- attrs = set(LanClientDevice.__protocol_attrs__)
432
+ attrs = protocol_attrs(LanClientDevice)
429
433
  assert "packet_filter" in attrs
430
434
  for absent in ("nat", "port_forwarding", "conntrack", "firewall_zones"):
431
435
  assert absent not in attrs, f"LanClientDevice unexpectedly aggregates {absent}"
@@ -433,7 +437,7 @@ def test_lan_client_firewall_facets() -> None:
433
437
 
434
438
  def test_wan_server_firewall_facets() -> None:
435
439
  """WanServerDevice aggregates packet_filter, nat, conntrack — no port-forwarding / zones."""
436
- attrs = set(WanServerDevice.__protocol_attrs__)
440
+ attrs = protocol_attrs(WanServerDevice)
437
441
  for present in ("packet_filter", "nat", "conntrack"):
438
442
  assert present in attrs
439
443
  for absent in ("port_forwarding", "firewall_zones"):
@@ -442,7 +446,7 @@ def test_wan_server_firewall_facets() -> None:
442
446
 
443
447
  def test_cpe_full_firewall_surface() -> None:
444
448
  """CpeDevice aggregates the complete firewall surface."""
445
- attrs = set(CpeDevice.__protocol_attrs__)
449
+ attrs = protocol_attrs(CpeDevice)
446
450
  for present in ("firewall", "nat", "conntrack", "firewall_zones"):
447
451
  assert present in attrs
448
452
  # PortForwarding is no longer a separate attribute — it's bundled into firewall.
@@ -455,7 +459,12 @@ def test_cpe_full_firewall_surface() -> None:
455
459
  def test_devices_without_firewall() -> None:
456
460
  """These archetypes must not pull in any of the firewall capabilities."""
457
461
  firewall_attrs = (
458
- "packet_filter", "firewall", "nat", "port_forwarding", "conntrack", "firewall_zones"
462
+ "packet_filter",
463
+ "firewall",
464
+ "nat",
465
+ "port_forwarding",
466
+ "conntrack",
467
+ "firewall_zones",
459
468
  )
460
469
  for archetype in (
461
470
  TftpDevice,
@@ -466,7 +475,7 @@ def test_devices_without_firewall() -> None:
466
475
  TrafficControllerDevice,
467
476
  IperfTrafficGeneratorDevice,
468
477
  ):
469
- attrs = set(archetype.__protocol_attrs__)
478
+ attrs = protocol_attrs(archetype)
470
479
  for absent in firewall_attrs:
471
480
  assert absent not in attrs, f"{archetype.__name__} unexpectedly aggregates {absent}"
472
481
 
@@ -497,10 +506,10 @@ def test_archetype_inherits_base_device_protocol(archetype: type) -> None:
497
506
  @pytest.mark.parametrize("archetype", _ALL_ARCHETYPES, ids=lambda a: a.__name__)
498
507
  def test_archetype_carries_universal_identity(archetype: type) -> None:
499
508
  """Every archetype's protocol-attrs include device_name and device_type."""
500
- attrs = set(archetype.__protocol_attrs__)
509
+ attrs = protocol_attrs(archetype)
501
510
  assert {"device_name", "device_type"} <= attrs, (
502
511
  f"{archetype.__name__} missing universal-identity attrs: "
503
- f"{ {'device_name', 'device_type'} - attrs}"
512
+ f"{ {'device_name', 'device_type'} - attrs }"
504
513
  )
505
514
 
506
515
 
@@ -529,36 +538,58 @@ def test_l3_switch_routed_registered() -> None:
529
538
 
530
539
  def test_l3_switch_is_strict_superset_of_l2() -> None:
531
540
  # 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__)
541
+ l2_attrs = protocol_attrs(L2Switch)
542
+ l3_attrs = protocol_attrs(L3Switch)
534
543
  assert l2_attrs <= l3_attrs, f"L3Switch missing L2 attrs: {l2_attrs - l3_attrs}"
535
544
  expected_l3 = {
536
- "routed_interfaces", "static_routes", "routing_read",
537
- "ospf", "interface_dhcp", "gateway_redundancy",
545
+ "routed_interfaces",
546
+ "static_routes",
547
+ "routing_read",
548
+ "ospf",
549
+ "interface_dhcp",
550
+ "gateway_redundancy",
538
551
  }
539
552
  assert expected_l3 <= l3_attrs, f"missing: {expected_l3 - l3_attrs}"
540
553
  assert "bgp" not in l3_attrs, "bgp belongs only on L3SwitchRouted"
541
554
 
542
555
 
543
556
  def test_l3_switch_routed_adds_bgp() -> None:
544
- attrs = set(L3SwitchRouted.__protocol_attrs__)
557
+ attrs = protocol_attrs(L3SwitchRouted)
545
558
  assert "bgp" in attrs
546
- assert set(L3Switch.__protocol_attrs__) <= attrs # still a superset of L3Switch
559
+ assert protocol_attrs(L3Switch) <= attrs # still a superset of L3Switch
547
560
 
548
561
 
549
562
  def test_l2_switch_aggregates_expected_capabilities() -> None:
550
563
  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",
564
+ "switch_ports",
565
+ "switch_vlans",
566
+ "spanning_tree",
567
+ "link_aggregation",
568
+ "port_poe",
569
+ "port_security",
570
+ "radius",
571
+ "first_hop_security",
572
+ "storm_control",
573
+ "switch_acl",
574
+ "discovery",
575
+ "mac_table",
576
+ "port_status",
577
+ "switch_qos",
578
+ "syslog",
579
+ "ntp",
580
+ "placement_ports",
555
581
  }
556
- actual = set(L2Switch.__protocol_attrs__)
582
+ actual = protocol_attrs(L2Switch)
557
583
  assert expected <= actual, f"missing: {expected - actual}"
558
584
  # host-substrate levers stay on the Linux twins, never on a managed switch
559
585
  for host_lever in (
560
- "conntrack", "pcap", "ip_interface", "nat",
561
- "packet_filter", "firewall_zones", "wan_admin",
586
+ "conntrack",
587
+ "pcap",
588
+ "ip_interface",
589
+ "nat",
590
+ "packet_filter",
591
+ "firewall_zones",
592
+ "wan_admin",
562
593
  ):
563
594
  assert host_lever not in actual, f"{host_lever} must not be on L2Switch"
564
595
 
@@ -570,19 +601,29 @@ def test_l3_switch_excludes_host_substrate_levers() -> None:
570
601
  Linux-host-shaped ``ip_interface``; firewall / NAT / conntrack belong on the
571
602
  CPE/router archetypes; wan_admin is a Linux-only forced link-down lever.
572
603
  """
573
- actual = set(L3Switch.__protocol_attrs__)
604
+ actual = protocol_attrs(L3Switch)
574
605
  for host_lever in (
575
- "conntrack", "pcap", "ip_interface", "nat",
576
- "packet_filter", "firewall_zones", "wan_admin",
606
+ "conntrack",
607
+ "pcap",
608
+ "ip_interface",
609
+ "nat",
610
+ "packet_filter",
611
+ "firewall_zones",
612
+ "wan_admin",
577
613
  ):
578
614
  assert host_lever not in actual, f"{host_lever} must not be on L3Switch"
579
615
 
580
616
 
581
617
  def test_l3_switch_routed_excludes_host_substrate_levers() -> None:
582
618
  """L3SwitchRouted (the BGP-capable variant) likewise excludes host-substrate levers."""
583
- actual = set(L3SwitchRouted.__protocol_attrs__)
619
+ actual = protocol_attrs(L3SwitchRouted)
584
620
  for host_lever in (
585
- "conntrack", "pcap", "ip_interface", "nat",
586
- "packet_filter", "firewall_zones", "wan_admin",
621
+ "conntrack",
622
+ "pcap",
623
+ "ip_interface",
624
+ "nat",
625
+ "packet_filter",
626
+ "firewall_zones",
627
+ "wan_admin",
587
628
  ):
588
629
  assert host_lever not in actual, f"{host_lever} must not be on L3SwitchRouted"
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from _helpers import protocol_attrs
5
6
  from testprotocols.dhcp_client import DhcpClient
6
7
  from testprotocols.dhcp_server import DhcpServer
7
8
  from testprotocols.models.dhcp import DhcpLeaseObservation
@@ -25,7 +26,7 @@ def test_dhcp_client_protocol_shape() -> None:
25
26
  "observe_lease",
26
27
  "release_observed_lease",
27
28
  }
28
- actual = set(DhcpClient.__protocol_attrs__)
29
+ actual = protocol_attrs(DhcpClient)
29
30
  assert expected <= actual, f"missing: {expected - actual}"
30
31
 
31
32
 
@@ -53,5 +54,5 @@ def test_dhcp_server_is_runtime_checkable() -> None:
53
54
  def test_dhcp_server_protocol_shape() -> None:
54
55
  """DhcpServer declares the expected method set."""
55
56
  expected = {"provision_cpe"}
56
- actual = set(DhcpServer.__protocol_attrs__)
57
+ actual = protocol_attrs(DhcpServer)
57
58
  assert expected <= actual, f"missing: {expected - actual}"