testprotocols 0.7.2__tar.gz → 0.8.0__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 (150) hide show
  1. {testprotocols-0.7.2/src/testprotocols.egg-info → testprotocols-0.8.0}/PKG-INFO +1 -1
  2. {testprotocols-0.7.2 → testprotocols-0.8.0}/pyproject.toml +1 -1
  3. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/__init__.py +10 -1
  4. testprotocols-0.8.0/src/testprotocols/config_ownership.py +21 -0
  5. testprotocols-0.8.0/src/testprotocols/device_info.py +22 -0
  6. testprotocols-0.8.0/src/testprotocols/devices/__init__.py +80 -0
  7. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/devices/base.py +3 -1
  8. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/devices/client.py +10 -31
  9. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/devices/sdwan.py +10 -11
  10. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/devices/switch.py +9 -5
  11. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/devices/voice.py +4 -21
  12. testprotocols-0.8.0/src/testprotocols/network_attachment.py +56 -0
  13. testprotocols-0.8.0/src/testprotocols/placement.py +44 -0
  14. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/sip_phone.py +18 -0
  15. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/sip_server.py +8 -0
  16. {testprotocols-0.7.2 → testprotocols-0.8.0/src/testprotocols.egg-info}/PKG-INFO +1 -1
  17. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols.egg-info/SOURCES.txt +6 -0
  18. testprotocols-0.8.0/tests/test_archetype_purity.py +59 -0
  19. testprotocols-0.8.0/tests/test_boundary_capability_templates.py +47 -0
  20. {testprotocols-0.7.2 → testprotocols-0.8.0}/tests/test_device_types.py +21 -6
  21. {testprotocols-0.7.2 → testprotocols-0.8.0}/tests/test_voice_wifi_templates.py +17 -0
  22. testprotocols-0.7.2/src/testprotocols/devices/__init__.py +0 -46
  23. {testprotocols-0.7.2 → testprotocols-0.8.0}/LICENSE +0 -0
  24. {testprotocols-0.7.2 → testprotocols-0.8.0}/NOTICE +0 -0
  25. {testprotocols-0.7.2 → testprotocols-0.8.0}/README.md +0 -0
  26. {testprotocols-0.7.2 → testprotocols-0.8.0}/setup.cfg +0 -0
  27. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/aftr_gateway.py +0 -0
  28. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/appliance_nat.py +0 -0
  29. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/appliance_uplinks.py +0 -0
  30. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/appliance_vlans.py +0 -0
  31. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/arp_client.py +0 -0
  32. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/bgp.py +0 -0
  33. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/conntrack.py +0 -0
  34. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/content_filtering.py +0 -0
  35. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/device_lifecycle.py +0 -0
  36. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/device_management.py +0 -0
  37. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/devices/cpe.py +0 -0
  38. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/devices/infra.py +0 -0
  39. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/devices/traffic.py +0 -0
  40. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/devices/wan.py +0 -0
  41. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/dhcp_client.py +0 -0
  42. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/dhcp_server.py +0 -0
  43. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/discovery.py +0 -0
  44. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/dns_client.py +0 -0
  45. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/file_transfer.py +0 -0
  46. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/firewall.py +0 -0
  47. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/firewall_zones.py +0 -0
  48. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/first_hop_security.py +0 -0
  49. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/gateway_redundancy.py +0 -0
  50. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/http_client.py +0 -0
  51. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/http_server.py +0 -0
  52. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/hw_console.py +0 -0
  53. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/interface_dhcp.py +0 -0
  54. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/ip_interface.py +0 -0
  55. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/ip_routing.py +0 -0
  56. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/iperf_client.py +0 -0
  57. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/iperf_generator.py +0 -0
  58. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/iperf_server.py +0 -0
  59. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/l3_firewall.py +0 -0
  60. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/l7_firewall.py +0 -0
  61. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/link_aggregation.py +0 -0
  62. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/mac_table.py +0 -0
  63. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/models/__init__.py +0 -0
  64. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/models/dhcp.py +0 -0
  65. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/models/firewall.py +0 -0
  66. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/models/impairment.py +0 -0
  67. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/models/l2_common.py +0 -0
  68. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/models/multicast.py +0 -0
  69. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/models/networking.py +0 -0
  70. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/models/packets.py +0 -0
  71. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/models/qoe.py +0 -0
  72. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/models/radius.py +0 -0
  73. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/models/sdwan_appliance.py +0 -0
  74. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/models/switch.py +0 -0
  75. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/models/switch_routing.py +0 -0
  76. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/models/tr069.py +0 -0
  77. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/models/traffic.py +0 -0
  78. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/models/wan_edge.py +0 -0
  79. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/models/wifi.py +0 -0
  80. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/multicast_client.py +0 -0
  81. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/nat.py +0 -0
  82. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/netem_controller.py +0 -0
  83. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/network_endpoint.py +0 -0
  84. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/network_probe.py +0 -0
  85. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/nmap_scanner.py +0 -0
  86. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/ntp_client.py +0 -0
  87. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/ntp_config.py +0 -0
  88. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/ospf.py +0 -0
  89. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/packet_filter.py +0 -0
  90. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/pcap_capture.py +0 -0
  91. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/pdu_controller.py +0 -0
  92. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/port_poe.py +0 -0
  93. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/port_security.py +0 -0
  94. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/port_status.py +0 -0
  95. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/py.typed +0 -0
  96. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/qoe_browser.py +0 -0
  97. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/radius_client.py +0 -0
  98. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/radius_server.py +0 -0
  99. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/reachability_responder.py +0 -0
  100. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/routed_interfaces.py +0 -0
  101. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/router.py +0 -0
  102. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/routing_read.py +0 -0
  103. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/sdwan_policy_manager.py +0 -0
  104. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/site_to_site_vpn.py +0 -0
  105. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/snmp_client.py +0 -0
  106. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/spanning_tree.py +0 -0
  107. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/static_routes.py +0 -0
  108. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/storm_control.py +0 -0
  109. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/streaming_server.py +0 -0
  110. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/switch_acl.py +0 -0
  111. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/switch_ports.py +0 -0
  112. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/switch_qos.py +0 -0
  113. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/switch_vlans.py +0 -0
  114. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/syslog_config.py +0 -0
  115. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/tftp_server.py +0 -0
  116. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/threat_prevention.py +0 -0
  117. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/tr069_client.py +0 -0
  118. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/tr069_server.py +0 -0
  119. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/traffic_shaping.py +0 -0
  120. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/upnp_client.py +0 -0
  121. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/vlan_client.py +0 -0
  122. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/wan_link_admin.py +0 -0
  123. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/wifi_bss.py +0 -0
  124. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/wifi_client.py +0 -0
  125. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/wifi_mesh.py +0 -0
  126. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/wifi_onboarding.py +0 -0
  127. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/wifi_radio.py +0 -0
  128. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/wifi_rf.py +0 -0
  129. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/wifi_stations.py +0 -0
  130. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols/wifi_transitions.py +0 -0
  131. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols.egg-info/dependency_links.txt +0 -0
  132. {testprotocols-0.7.2 → testprotocols-0.8.0}/src/testprotocols.egg-info/top_level.txt +0 -0
  133. {testprotocols-0.7.2 → testprotocols-0.8.0}/tests/test_appliance_vlans.py +0 -0
  134. {testprotocols-0.7.2 → testprotocols-0.8.0}/tests/test_cpe_templates.py +0 -0
  135. {testprotocols-0.7.2 → testprotocols-0.8.0}/tests/test_dhcp_templates.py +0 -0
  136. {testprotocols-0.7.2 → testprotocols-0.8.0}/tests/test_firewall_templates.py +0 -0
  137. {testprotocols-0.7.2 → testprotocols-0.8.0}/tests/test_infra_templates.py +0 -0
  138. {testprotocols-0.7.2 → testprotocols-0.8.0}/tests/test_ip_interface.py +0 -0
  139. {testprotocols-0.7.2 → testprotocols-0.8.0}/tests/test_ip_routing.py +0 -0
  140. {testprotocols-0.7.2 → testprotocols-0.8.0}/tests/test_network_tool_templates.py +0 -0
  141. {testprotocols-0.7.2 → testprotocols-0.8.0}/tests/test_package_imports.py +0 -0
  142. {testprotocols-0.7.2 → testprotocols-0.8.0}/tests/test_radius_templates.py +0 -0
  143. {testprotocols-0.7.2 → testprotocols-0.8.0}/tests/test_sdwan_appliance_models.py +0 -0
  144. {testprotocols-0.7.2 → testprotocols-0.8.0}/tests/test_sdwan_appliance_templates.py +0 -0
  145. {testprotocols-0.7.2 → testprotocols-0.8.0}/tests/test_switch_models.py +0 -0
  146. {testprotocols-0.7.2 → testprotocols-0.8.0}/tests/test_switch_routing_models.py +0 -0
  147. {testprotocols-0.7.2 → testprotocols-0.8.0}/tests/test_switch_templates.py +0 -0
  148. {testprotocols-0.7.2 → testprotocols-0.8.0}/tests/test_traffic_templates.py +0 -0
  149. {testprotocols-0.7.2 → testprotocols-0.8.0}/tests/test_wan_edge_templates.py +0 -0
  150. {testprotocols-0.7.2 → testprotocols-0.8.0}/tests/test_wifi_templates.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: testprotocols
3
- Version: 0.7.2
3
+ Version: 0.8.0
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.2"
3
+ version = "0.8.0"
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.7.2"
3
+ __version__ = "0.8.0"
4
4
 
5
5
  from testprotocols.aftr_gateway import AftrGateway
6
6
  from testprotocols.appliance_nat import ApplianceNat
@@ -8,8 +8,10 @@ from testprotocols.appliance_uplinks import ApplianceUplinks
8
8
  from testprotocols.appliance_vlans import ApplianceVlans
9
9
  from testprotocols.arp_client import ArpClient
10
10
  from testprotocols.bgp import Bgp
11
+ from testprotocols.config_ownership import ConfigOwnership
11
12
  from testprotocols.conntrack import Conntrack, ConntrackWhiteBox
12
13
  from testprotocols.content_filtering import ContentFiltering
14
+ from testprotocols.device_info import DeviceInfo
13
15
  from testprotocols.device_lifecycle import DeviceLifecycle
14
16
  from testprotocols.device_management import DeviceManagement
15
17
  from testprotocols.devices.base import BaseDeviceProtocol
@@ -60,6 +62,7 @@ from testprotocols.mac_table import MacTable
60
62
  from testprotocols.multicast_client import MulticastClient
61
63
  from testprotocols.nat import Nat
62
64
  from testprotocols.netem_controller import NetemController
65
+ from testprotocols.network_attachment import NetworkAttachment
63
66
  from testprotocols.nmap_scanner import NmapScanner
64
67
  from testprotocols.ntp_client import NtpClient
65
68
  from testprotocols.ntp_config import NtpConfig
@@ -67,6 +70,7 @@ from testprotocols.ospf import Ospf
67
70
  from testprotocols.packet_filter import PacketFilter, PacketFilterWhiteBox
68
71
  from testprotocols.pcap_capture import PcapCapture
69
72
  from testprotocols.pdu_controller import PduController
73
+ from testprotocols.placement import PlacementPipe, PlacementPorts
70
74
  from testprotocols.port_poe import PortPoe
71
75
  from testprotocols.port_security import PortSecurity
72
76
  from testprotocols.port_status import PortStatus
@@ -116,10 +120,12 @@ __all__ = [
116
120
  "ArpClient",
117
121
  "BaseDeviceProtocol",
118
122
  "Bgp",
123
+ "ConfigOwnership",
119
124
  "Conntrack",
120
125
  "ConntrackWhiteBox",
121
126
  "ContentFiltering",
122
127
  "CpeDevice",
128
+ "DeviceInfo",
123
129
  "DeviceLifecycle",
124
130
  "DeviceManagement",
125
131
  "DhcpClient",
@@ -153,6 +159,7 @@ __all__ = [
153
159
  "MulticastClient",
154
160
  "Nat",
155
161
  "NetemController",
162
+ "NetworkAttachment",
156
163
  "NmapScanner",
157
164
  "NtpClient",
158
165
  "NtpConfig",
@@ -161,6 +168,8 @@ __all__ = [
161
168
  "PacketFilterWhiteBox",
162
169
  "PcapCapture",
163
170
  "PduController",
171
+ "PlacementPipe",
172
+ "PlacementPorts",
164
173
  "PortPoe",
165
174
  "PortSecurity",
166
175
  "PortStatus",
@@ -0,0 +1,21 @@
1
+ """Config ownership template.
2
+
3
+ Defines the abstract contract for session write-ownership of a device's
4
+ network configuration — e.g. a warm-spare member whose primary owns the
5
+ writes. The natural home for future redundancy-role facts.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Protocol, runtime_checkable
11
+
12
+
13
+ @runtime_checkable
14
+ class ConfigOwnership(Protocol):
15
+ """Abstract contract for network-config write ownership."""
16
+
17
+ @property
18
+ def manages_network(self) -> bool:
19
+ """Whether this session owns the device's network configuration.
20
+ False for a warm-spare member whose primary owns the writes."""
21
+ ...
@@ -0,0 +1,22 @@
1
+ """Device info template.
2
+
3
+ Defines the abstract contract for a device's inventory identity — today the
4
+ hardware model. Vendor-neutral metadata the driver resolves from its
5
+ management API, inventory config, or class knowledge; the coverage axis for
6
+ model-parameterised tests. Extensible with further identity facts (serial,
7
+ firmware version) when a consumer needs them.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Protocol, runtime_checkable
13
+
14
+
15
+ @runtime_checkable
16
+ class DeviceInfo(Protocol):
17
+ """Abstract contract for device inventory identity."""
18
+
19
+ @property
20
+ def model(self) -> str:
21
+ """Hardware model identifier (e.g. ``"MX250"``)."""
22
+ ...
@@ -0,0 +1,80 @@
1
+ """Device archetype registry — maps inventory device_type strings to Protocols."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ from dataclasses import dataclass
7
+ from typing import Any, cast, get_type_hints
8
+
9
+ _registry: dict[str, DeviceTypeSpec] = {}
10
+
11
+ #: The sole sanctioned non-capability members — BaseDeviceProtocol's universal
12
+ #: identity pair (see devices/base.py).
13
+ IDENTITY_MEMBERS = frozenset({"device_name", "device_type"})
14
+
15
+
16
+ def non_capability_members(protocol: type) -> frozenset[str]:
17
+ """Members of *protocol* that are neither capability Protocols nor identity.
18
+
19
+ A capability member is annotated with a ``typing.Protocol`` class. Scalar
20
+ annotations (``str``, ``bool``, ``tuple``…) and property-declared members
21
+ (which carry no annotation at all) are both offenders. ``__protocol_attrs__``
22
+ is the CPython 3.12 runtime detail ``typing.get_protocol_members``
23
+ supersedes in 3.13; one cast keeps that knowledge here.
24
+ """
25
+ hints = get_type_hints(protocol)
26
+ members: frozenset[str] = frozenset(cast("Any", protocol).__protocol_attrs__)
27
+ return frozenset(
28
+ member
29
+ for member in members - IDENTITY_MEMBERS
30
+ if not getattr(hints.get(member), "_is_protocol", False)
31
+ )
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class DeviceTypeSpec:
36
+ """Registration record: inventory string -> @runtime_checkable Protocol class."""
37
+
38
+ name: str
39
+ protocol: type
40
+
41
+
42
+ def register_device_type(name: str, protocol: type) -> DeviceTypeSpec:
43
+ """Register *protocol* as the contract for inventory devices typed *name*.
44
+
45
+ Fails closed on archetype drift: every member must be a capability
46
+ Protocol (the BaseDeviceProtocol identity pair excepted) — see
47
+ docs/architecture/capability-only-archetypes.md. Returns the spec; also
48
+ stored in the module-level registry so ``get_device_type(name)`` can look
49
+ it up by inventory string.
50
+ """
51
+ offenders = non_capability_members(protocol)
52
+ if offenders:
53
+ raise TypeError(
54
+ f"device archetype {protocol.__name__!r} ({name!r}) declares "
55
+ f"non-capability members {sorted(offenders)}; archetypes compose "
56
+ "capability protocols only — move these into a capability protocol "
57
+ "(docs/architecture/capability-only-archetypes.md)"
58
+ )
59
+ spec = DeviceTypeSpec(name=name, protocol=protocol)
60
+ _registry[name] = spec
61
+ return spec
62
+
63
+
64
+ def get_device_type(name: str) -> DeviceTypeSpec | None:
65
+ """Return the spec for inventory *name*, or None if not registered."""
66
+ return _registry.get(name)
67
+
68
+
69
+ def all_device_types() -> dict[str, DeviceTypeSpec]:
70
+ """Return a copy of the full registry."""
71
+ return dict(_registry)
72
+
73
+
74
+ # Auto-import all archetype modules to trigger registration as a side effect.
75
+ # ``importlib.import_module`` avoids the unused-import / out-of-order-import
76
+ # friction that direct ``from … import …`` statements would create here, and
77
+ # keeps the import section at the top of the file as PEP 8 prefers.
78
+ for _mod in ("client", "cpe", "infra", "sdwan", "switch", "traffic", "voice", "wan"):
79
+ importlib.import_module(f"testprotocols.devices.{_mod}")
80
+ del _mod
@@ -21,7 +21,9 @@ class BaseDeviceProtocol(Protocol):
21
21
  Deliberately scoped to the universal pair — name + type. Anything a
22
22
  specific framework wants to expose (vitro's ``config`` /
23
23
  ``get_interactive_consoles``, boardfarm's lifecycle hooks, etc.) is
24
- the framework's own concern. Frameworks declare a connector Protocol
24
+ the framework's own concern. Every other archetype member must be a
25
+ capability Protocol — enforced by ``register_device_type``; see
26
+ docs/architecture/capability-only-archetypes.md. Frameworks declare a connector Protocol
25
27
  on their side that extends this base; testbed plugins compose
26
28
  archetype + framework-connector into a combined Protocol where step
27
29
  defs need both.
@@ -17,12 +17,14 @@ from testprotocols.ip_routing import IpRouting
17
17
  from testprotocols.iperf_client import IperfClient
18
18
  from testprotocols.iperf_server import IperfServer
19
19
  from testprotocols.multicast_client import MulticastClient
20
+ from testprotocols.network_attachment import NetworkAttachment
20
21
  from testprotocols.network_endpoint import NetworkEndpoint
21
22
  from testprotocols.network_probe import NetworkProbe
22
23
  from testprotocols.nmap_scanner import NmapScanner
23
24
  from testprotocols.ntp_client import NtpClient
24
25
  from testprotocols.packet_filter import PacketFilter
25
26
  from testprotocols.pcap_capture import PcapCapture
27
+ from testprotocols.placement import PlacementPipe
26
28
  from testprotocols.qoe_browser import QoeBrowser
27
29
  from testprotocols.reachability_responder import ReachabilityResponder
28
30
  from testprotocols.syslog_config import SyslogConfig
@@ -128,37 +130,14 @@ class QoeMeasurementClientDevice(QoeClientDevice, Protocol):
128
130
  responder: ReachabilityResponder
129
131
  pcap: PcapCapture
130
132
 
131
- test_ip: str
132
- """The client's test-plane address as a CIDR (e.g. ``"10.1.30.50/24"``), empty
133
- if the client is not homed to a test segment. Vendor-neutral metadata the
134
- driver resolves from its homing config — read it here rather than reaching into
135
- a framework-specific config object."""
136
-
137
- attachment_segment: str
138
- """The declared segment LABEL of a guest-attached client (a client whose
139
- test leg sits on a segment the testbed does not manage), empty for managed
140
- homing or when undeclared. A stable role name (e.g. ``"provider-vpn"``),
141
- never an address: it lets tests resolve "the client on segment X" by
142
- declaration when address facts are foreign-owned or discovered late.
143
- Vendor-neutral metadata from the driver's attachment config."""
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."""
133
+ attachment: NetworkAttachment
134
+ """Homing metadata — test/mgmt plane addresses and the declared guest
135
+ attachment (segment label + subnet). See ``NetworkAttachment`` for the
136
+ boundary against ``NetworkEndpoint``."""
137
+
138
+ pipe: PlacementPipe
139
+ """This client's declared placement pipe (dedicated access port on a
140
+ managed switch); counterpart of the switch's ``placement`` authorization."""
162
141
 
163
142
 
164
143
  register_device_type("linux_lan_client", LanClientDevice)
@@ -8,8 +8,10 @@ from testprotocols.appliance_nat import ApplianceNat
8
8
  from testprotocols.appliance_uplinks import ApplianceUplinks
9
9
  from testprotocols.appliance_vlans import ApplianceVlans
10
10
  from testprotocols.bgp import Bgp
11
+ from testprotocols.config_ownership import ConfigOwnership
11
12
  from testprotocols.conntrack import Conntrack
12
13
  from testprotocols.content_filtering import ContentFiltering
14
+ from testprotocols.device_info import DeviceInfo
13
15
  from testprotocols.devices import register_device_type
14
16
  from testprotocols.devices.base import BaseDeviceProtocol
15
17
  from testprotocols.ip_interface import IpInterface
@@ -93,17 +95,14 @@ class SdwanApplianceDevice(BaseDeviceProtocol, Protocol):
93
95
  lan: ApplianceVlans
94
96
  syslog: SyslogConfig
95
97
 
96
- model: str
97
- """Hardware model identifier (e.g. ``"MX250"``) — vendor-neutral metadata the
98
- driver resolves from its management API / inventory. The coverage axis for
99
- model-parameterised tests; read it here rather than reaching into a
100
- framework-specific config object."""
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."""
98
+ info: DeviceInfo
99
+ """Inventory identity (hardware model, e.g. ``"MX250"``) — the coverage
100
+ axis for model-parameterised tests, resolved by the driver from its
101
+ management API / inventory."""
102
+
103
+ ownership: ConfigOwnership
104
+ """Whether this session owns the appliance's network configuration —
105
+ False for a warm-spare member whose primary owns the writes."""
107
106
 
108
107
 
109
108
  register_device_type("sdwan_appliance", SdwanApplianceDevice)
@@ -12,6 +12,7 @@ from __future__ import annotations
12
12
  from typing import Protocol, runtime_checkable
13
13
 
14
14
  from testprotocols.bgp import Bgp
15
+ from testprotocols.device_info import DeviceInfo
15
16
  from testprotocols.devices import register_device_type
16
17
  from testprotocols.devices.base import BaseDeviceProtocol
17
18
  from testprotocols.discovery import Discovery
@@ -22,6 +23,7 @@ from testprotocols.link_aggregation import LinkAggregation
22
23
  from testprotocols.mac_table import MacTable
23
24
  from testprotocols.ntp_config import NtpConfig
24
25
  from testprotocols.ospf import Ospf
26
+ from testprotocols.placement import PlacementPorts
25
27
  from testprotocols.port_poe import PortPoe
26
28
  from testprotocols.port_security import PortSecurity
27
29
  from testprotocols.port_status import PortStatus
@@ -68,11 +70,13 @@ class L2Switch(BaseDeviceProtocol, Protocol):
68
70
  syslog: SyslogConfig
69
71
  ntp: NtpConfig
70
72
 
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."""
73
+ placement: PlacementPorts
74
+ """Placement-write authorization — which ports a testbed may re-VLAN to
75
+ place a client; counterpart of a client's ``pipe`` declaration."""
76
+
77
+ info: DeviceInfo
78
+ """Inventory identity (hardware model) — the coverage axis for
79
+ model-parameterised switch tests."""
76
80
 
77
81
 
78
82
  register_device_type("managed_switch_l2", L2Switch)
@@ -21,20 +21,8 @@ class SipPhoneDevice(BaseDeviceProtocol, Protocol):
21
21
  Used to drive call-control scenarios (register / invite / cancel / bye) and
22
22
  media-plane verification against a SIP server under test.
23
23
 
24
- A phone has one or more SIP accounts (lines). Most testbeds run single-line
25
- phones; multi-line phones expose additional entries in ``aors``.
26
-
27
- ``aors`` is the list of registered SIP URIs in RFC 3261 §6 address-of-record
28
- form (``sip:user@host`` with optional ``;params``). ``host`` may be an FQDN,
29
- an IPv4 literal (``10.0.0.5``), or an IPv6 bracketed literal
30
- (``[2001:db8::5]``) — the driver decides which form matches its testbed's
31
- address-resolution context. The list is never empty; ``aors[0]`` is the
32
- primary line that single-line operations (``sip_phone.dial``,
33
- ``sip_phone.answer``, MWI subscriptions, …) act on.
34
-
35
- ``number`` is the user-part of ``aors[0]`` — the SIP phone's E.164 /
36
- dial-string identifier for its primary line. Provided as a convenience so
37
- single-line callers don't have to parse SIP URIs.
24
+ Line identity — ``number`` / ``aors`` — lives on the ``SipPhone``
25
+ capability protocol.
38
26
 
39
27
  ``device_name`` / ``device_type`` come from :class:`BaseDeviceProtocol`.
40
28
  """
@@ -42,8 +30,6 @@ class SipPhoneDevice(BaseDeviceProtocol, Protocol):
42
30
  sip_phone: SipPhone
43
31
  ip_interface: IpInterface
44
32
  dhcp_client: DhcpClient
45
- number: str
46
- aors: list[str]
47
33
 
48
34
 
49
35
  @runtime_checkable
@@ -53,16 +39,13 @@ class SipServerDevice(BaseDeviceProtocol, Protocol):
53
39
  Provides the server-side SIP surface plus packet capture and file transfer
54
40
  for inspecting and exporting signalling exchanges.
55
41
 
56
- ``aor_domain`` is the host part that callers should embed in SIP AORs
57
- targeting this server (``sip:user@<aor_domain>``). May be an FQDN, an
58
- IPv4 literal, or an IPv6 bracketed literal — the driver decides which
59
- form matches its testbed's address-resolution context.
42
+ The AOR host part — ``aor_domain`` — lives on the ``SipServer``
43
+ capability protocol.
60
44
  """
61
45
 
62
46
  sip_server: SipServer
63
47
  pcap: PcapCapture
64
48
  file_transfer: FileTransfer
65
- aor_domain: str
66
49
 
67
50
 
68
51
  register_device_type("linux_sip_phone", SipPhoneDevice)
@@ -0,0 +1,56 @@
1
+ """Network attachment template.
2
+
3
+ Defines the abstract contract for an endpoint's *declared or discovered
4
+ homing* onto the testbed's network planes: its test-plane address, its
5
+ management-plane address, and — for a guest-attached endpoint (one whose
6
+ test leg sits on a segment the testbed does not manage) — the declared
7
+ segment label and subnet.
8
+
9
+ Distinct from ``NetworkEndpoint``: that is the live role-address query
10
+ ("give me the WAN address, now"), answered by whatever means the driver
11
+ chooses. ``NetworkAttachment`` is homing *metadata* the driver resolves
12
+ from its own config or discovers at attach time (e.g. a DHCP lease),
13
+ published so consumers never reach into a framework-specific config
14
+ object. Archetypes whose drivers already answer addressing via
15
+ ``NetworkEndpoint`` should not also compose this; it earns its place
16
+ where declared homing (segment labels, plane separation) is the
17
+ mechanism under test.
18
+
19
+ All values are vendor-neutral; empty string means undeclared/not homed.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from typing import Protocol, runtime_checkable
25
+
26
+
27
+ @runtime_checkable
28
+ class NetworkAttachment(Protocol):
29
+ """Abstract contract for an endpoint's homing metadata."""
30
+
31
+ @property
32
+ def test_ip(self) -> str:
33
+ """The endpoint's test-plane address as a CIDR (e.g. ``"10.1.30.50/24"``),
34
+ empty if not homed to a test segment. May be discovered at attach time
35
+ (e.g. from a DHCP lease) rather than declared."""
36
+ ...
37
+
38
+ @property
39
+ def mgmt_ip(self) -> str:
40
+ """The endpoint's management-plane address, empty when undeclared.
41
+ Published so isolation checks never consume a framework-specific
42
+ config object."""
43
+ ...
44
+
45
+ @property
46
+ def segment(self) -> str:
47
+ """Declared segment LABEL of a guest attachment — a stable role name
48
+ (e.g. ``"provider-vpn"``), never an address — empty for managed homing
49
+ or when undeclared. Lets tests resolve "the endpoint on segment X" by
50
+ declaration when address facts are foreign-owned or discovered late."""
51
+ ...
52
+
53
+ @property
54
+ def subnet(self) -> str:
55
+ """Declared subnet (CIDR) of a guest attachment, empty when undeclared."""
56
+ ...
@@ -0,0 +1,44 @@
1
+ """Client-placement templates — both halves of the placement mechanism.
2
+
3
+ A *placement pipe* is a client's dedicated access port on a managed switch:
4
+ re-VLANing that port is the client's placement knob. The mechanism has two
5
+ sides owned by two devices: the **client** declares which switch/port carries
6
+ its pipe (``PlacementPipe`` — topology fact), and the **switch** declares
7
+ which ports a testbed may re-VLAN at all (``PlacementPorts`` — write
8
+ authorization, owned by the resource's owner). Pipe ownership and write
9
+ authorization are deliberately independent declarations.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Protocol, runtime_checkable
15
+
16
+
17
+ @runtime_checkable
18
+ class PlacementPipe(Protocol):
19
+ """Abstract contract for a client's declared placement pipe."""
20
+
21
+ @property
22
+ def switch(self) -> str:
23
+ """Name of the switch carrying this client's dedicated access port,
24
+ empty when the client has no declared pipe."""
25
+ ...
26
+
27
+ @property
28
+ def port(self) -> str:
29
+ """The dedicated access port whose VLAN is this client's placement
30
+ knob, empty when the client has no declared pipe."""
31
+ ...
32
+
33
+
34
+ @runtime_checkable
35
+ class PlacementPorts(Protocol):
36
+ """Abstract contract for a switch's placement-write authorization."""
37
+
38
+ @property
39
+ def ports(self) -> tuple[str, ...]:
40
+ """Designated ports a testbed may re-VLAN to place a client; empty
41
+ means no placement write is permitted on this switch. Independent of
42
+ which device's pipe enters a port: pipe ownership is topology, this is
43
+ write authorization — declared by the switch, the resource's owner."""
44
+ ...
@@ -13,6 +13,24 @@ from typing import Protocol, runtime_checkable
13
13
  class SipPhone(Protocol):
14
14
  """Abstract contract for SIP phone operations."""
15
15
 
16
+ @property
17
+ def number(self) -> str:
18
+ """The user-part of ``aors[0]`` — the phone's E.164 / dial-string
19
+ identifier for its primary line. A convenience so single-line callers
20
+ don't have to parse SIP URIs."""
21
+ ...
22
+
23
+ @property
24
+ def aors(self) -> list[str]:
25
+ """Registered SIP URIs in RFC 3261 §6 address-of-record form
26
+ (``sip:user@host`` with optional ``;params``). ``host`` may be an FQDN,
27
+ an IPv4 literal, or an IPv6 bracketed literal — the driver decides
28
+ which form matches its testbed's address-resolution context. Never
29
+ empty once configured; ``aors[0]`` is the primary line that single-line
30
+ operations (``dial``, ``answer``, MWI subscriptions, …) act on.
31
+ Multi-line phones expose additional entries."""
32
+ ...
33
+
16
34
  def phone_start(self) -> None:
17
35
  """Start the SIP phone software."""
18
36
  ...
@@ -32,6 +32,14 @@ class SipServer(Protocol):
32
32
  """IPv6 address of the SIP server, or None if not configured."""
33
33
  ...
34
34
 
35
+ @property
36
+ def aor_domain(self) -> str:
37
+ """The host part callers should embed in SIP AORs targeting this
38
+ server (``sip:user@<aor_domain>``). May be an FQDN, an IPv4 literal,
39
+ or an IPv6 bracketed literal — the driver decides which form matches
40
+ its testbed's address-resolution context."""
41
+ ...
42
+
35
43
  # ------------------------------------------------------------------
36
44
  # Abstract methods
37
45
  # ------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: testprotocols
3
- Version: 0.7.2
3
+ Version: 0.8.0
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>
@@ -9,8 +9,10 @@ src/testprotocols/appliance_uplinks.py
9
9
  src/testprotocols/appliance_vlans.py
10
10
  src/testprotocols/arp_client.py
11
11
  src/testprotocols/bgp.py
12
+ src/testprotocols/config_ownership.py
12
13
  src/testprotocols/conntrack.py
13
14
  src/testprotocols/content_filtering.py
15
+ src/testprotocols/device_info.py
14
16
  src/testprotocols/device_lifecycle.py
15
17
  src/testprotocols/device_management.py
16
18
  src/testprotocols/dhcp_client.py
@@ -38,6 +40,7 @@ src/testprotocols/mac_table.py
38
40
  src/testprotocols/multicast_client.py
39
41
  src/testprotocols/nat.py
40
42
  src/testprotocols/netem_controller.py
43
+ src/testprotocols/network_attachment.py
41
44
  src/testprotocols/network_endpoint.py
42
45
  src/testprotocols/network_probe.py
43
46
  src/testprotocols/nmap_scanner.py
@@ -47,6 +50,7 @@ src/testprotocols/ospf.py
47
50
  src/testprotocols/packet_filter.py
48
51
  src/testprotocols/pcap_capture.py
49
52
  src/testprotocols/pdu_controller.py
53
+ src/testprotocols/placement.py
50
54
  src/testprotocols/port_poe.py
51
55
  src/testprotocols/port_security.py
52
56
  src/testprotocols/port_status.py
@@ -120,6 +124,8 @@ src/testprotocols/models/traffic.py
120
124
  src/testprotocols/models/wan_edge.py
121
125
  src/testprotocols/models/wifi.py
122
126
  tests/test_appliance_vlans.py
127
+ tests/test_archetype_purity.py
128
+ tests/test_boundary_capability_templates.py
123
129
  tests/test_cpe_templates.py
124
130
  tests/test_device_types.py
125
131
  tests/test_dhcp_templates.py
@@ -0,0 +1,59 @@
1
+ """Archetype-purity drift gate.
2
+
3
+ Device archetypes must compose capability protocols only; the sole sanctioned
4
+ exception is the BaseDeviceProtocol identity pair. Enforced twice: here (the
5
+ merge gate — parametrized over the registry, so every future archetype is
6
+ covered the moment it is registered) and in register_device_type itself (the
7
+ import-time gate that also covers plugin-local archetypes downstream). See
8
+ docs/architecture/capability-only-archetypes.md.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Protocol, runtime_checkable
14
+
15
+ import pytest
16
+ from testprotocols.devices import (
17
+ DeviceTypeSpec,
18
+ all_device_types,
19
+ get_device_type,
20
+ non_capability_members,
21
+ register_device_type,
22
+ )
23
+ from testprotocols.devices.base import BaseDeviceProtocol
24
+
25
+
26
+ @pytest.mark.parametrize(
27
+ ("name", "spec"),
28
+ sorted(all_device_types().items()),
29
+ ids=lambda v: v if isinstance(v, str) else "",
30
+ )
31
+ def test_archetype_members_are_capability_protocols(name: str, spec: DeviceTypeSpec) -> None:
32
+ offenders = non_capability_members(spec.protocol)
33
+ assert not offenders, (
34
+ f"{name}: non-capability members {sorted(offenders)} — archetypes compose "
35
+ "capability protocols only (docs/architecture/capability-only-archetypes.md)"
36
+ )
37
+
38
+
39
+ def test_checker_detects_scalar_members() -> None:
40
+ """The checker flags annotated scalars AND property-declared scalars."""
41
+
42
+ @runtime_checkable
43
+ class _Rogue(BaseDeviceProtocol, Protocol):
44
+ rogue_scalar: str
45
+
46
+ @property
47
+ def rogue_property(self) -> int: ...
48
+
49
+ assert non_capability_members(_Rogue) == {"rogue_scalar", "rogue_property"}
50
+
51
+
52
+ def test_register_rejects_non_capability_archetype() -> None:
53
+ @runtime_checkable
54
+ class _Rogue(BaseDeviceProtocol, Protocol):
55
+ rogue_scalar: str
56
+
57
+ with pytest.raises(TypeError, match="rogue_scalar"):
58
+ register_device_type("_rogue_purity_probe", _Rogue)
59
+ assert get_device_type("_rogue_purity_probe") is None, "must not register on failure"