python-netgear-switch-library 0.0.post154__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. netgear_switch/__init__.py +132 -0
  2. netgear_switch/_dispatch.py +178 -0
  3. netgear_switch/_version.py +24 -0
  4. netgear_switch/aio_api.py +529 -0
  5. netgear_switch/cli/__init__.py +1 -0
  6. netgear_switch/cli/capture.py +131 -0
  7. netgear_switch/cli/context.py +39 -0
  8. netgear_switch/cli/format.py +201 -0
  9. netgear_switch/cli/main.py +484 -0
  10. netgear_switch/cli/resolve.py +108 -0
  11. netgear_switch/cli/safety.py +71 -0
  12. netgear_switch/config.py +184 -0
  13. netgear_switch/errors.py +52 -0
  14. netgear_switch/http_read.py +174 -0
  15. netgear_switch/http_write.py +420 -0
  16. netgear_switch/models.py +156 -0
  17. netgear_switch/nsdp_read.py +221 -0
  18. netgear_switch/nsdp_write.py +315 -0
  19. netgear_switch/protocols/__init__.py +1 -0
  20. netgear_switch/protocols/http/__init__.py +1 -0
  21. netgear_switch/protocols/http/crypt.py +29 -0
  22. netgear_switch/protocols/http/endpoints.py +165 -0
  23. netgear_switch/protocols/http/forms.py +77 -0
  24. netgear_switch/protocols/http/parse.py +238 -0
  25. netgear_switch/protocols/http/session.py +29 -0
  26. netgear_switch/protocols/nsdp/__init__.py +7 -0
  27. netgear_switch/protocols/nsdp/auth.py +33 -0
  28. netgear_switch/protocols/nsdp/client.py +67 -0
  29. netgear_switch/protocols/nsdp/parsers.py +209 -0
  30. netgear_switch/protocols/nsdp/protocol.py +201 -0
  31. netgear_switch/protocols/nsdp/types.py +137 -0
  32. netgear_switch/protocols/nsdp/write.py +98 -0
  33. netgear_switch/protocols/snmp/__init__.py +1 -0
  34. netgear_switch/protocols/snmp/client.py +88 -0
  35. netgear_switch/protocols/snmp/oids.py +125 -0
  36. netgear_switch/protocols/snmp/parse.py +777 -0
  37. netgear_switch/protocols/snmp/write.py +112 -0
  38. netgear_switch/py.typed +0 -0
  39. netgear_switch/registry.py +227 -0
  40. netgear_switch/snmp_read.py +226 -0
  41. netgear_switch/snmp_write.py +625 -0
  42. netgear_switch/sync_api.py +557 -0
  43. netgear_switch/transport/__init__.py +1 -0
  44. netgear_switch/transport/aio/__init__.py +1 -0
  45. netgear_switch/transport/aio/nsdp_udp.py +152 -0
  46. netgear_switch/transport/aio/snmp_pysnmp.py +247 -0
  47. netgear_switch/transport/http/__init__.py +1 -0
  48. netgear_switch/transport/http/client.py +217 -0
  49. netgear_switch/transport/sync/__init__.py +1 -0
  50. netgear_switch/transport/sync/nsdp_udp.py +109 -0
  51. netgear_switch/transport/sync/snmp_netsnmp_cli.py +257 -0
  52. netgear_switch/virtual/__init__.py +8 -0
  53. netgear_switch/virtual/faces/__init__.py +2 -0
  54. netgear_switch/virtual/faces/http.py +164 -0
  55. netgear_switch/virtual/faces/mibview.py +92 -0
  56. netgear_switch/virtual/faces/nsdp.py +124 -0
  57. netgear_switch/virtual/faces/snmp.py +412 -0
  58. netgear_switch/virtual/seed.py +220 -0
  59. netgear_switch/virtual/server.py +106 -0
  60. netgear_switch/virtual/state.py +615 -0
  61. netgear_switch/virtual/web.py +210 -0
  62. python_netgear_switch_library-0.0.post154.dist-info/METADATA +85 -0
  63. python_netgear_switch_library-0.0.post154.dist-info/RECORD +66 -0
  64. python_netgear_switch_library-0.0.post154.dist-info/WHEEL +4 -0
  65. python_netgear_switch_library-0.0.post154.dist-info/entry_points.txt +2 -0
  66. python_netgear_switch_library-0.0.post154.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,132 @@
1
+ """Python Netgear Switch Interface Library.
2
+
3
+ Query and control Netgear switches over SNMP, NSDP and the HTTP web UI
4
+ behind one model-driven API.
5
+ """
6
+
7
+ from importlib.metadata import PackageNotFoundError
8
+ from importlib.metadata import version as _pkg_version
9
+
10
+ from .aio_api import AsyncSwitch, async_detect_model
11
+ from .config import (
12
+ SwitchConfig,
13
+ ensure_secure_file,
14
+ load_inventory,
15
+ resolve_secret,
16
+ )
17
+ from .errors import (
18
+ ConfigError,
19
+ CredentialError,
20
+ HttpAuthError,
21
+ HttpError,
22
+ HttpUnexpectedPageError,
23
+ NetgearSwitchError,
24
+ ProtectedPortError,
25
+ UnknownModelError,
26
+ UnsupportedCapabilityError,
27
+ WriteVerificationError,
28
+ )
29
+ from .models import (
30
+ DetectedModel,
31
+ IpMode,
32
+ LLDPNeighbor,
33
+ MacEntry,
34
+ MgmtIpConfig,
35
+ PoEDetect,
36
+ PoEStatus,
37
+ PortStats,
38
+ PortStatus,
39
+ Sensor,
40
+ SwitchData,
41
+ VLANInfo,
42
+ VlanMode,
43
+ )
44
+ from .protocols.nsdp.types import (
45
+ LinkSpeed as NsdpLinkSpeed,
46
+ )
47
+ from .protocols.nsdp.types import (
48
+ NsdpDevice,
49
+ NsdpIgmpSnooping,
50
+ NsdpPortMirroring,
51
+ NsdpPortPvid,
52
+ NsdpPortStatistics,
53
+ NsdpPortStatus,
54
+ NsdpVlanMembership,
55
+ )
56
+ from .protocols.nsdp.types import (
57
+ VLANEngine as NsdpVlanEngine,
58
+ )
59
+ from .registry import MODELS, Backend, SwitchClass, SwitchModel, get_model
60
+ from .sync_api import SyncSwitch, detect_model
61
+
62
+ try:
63
+ # Normal case: the package is installed (editable or wheel) and its
64
+ # dist-info carries the version hatch-vcs derived from git at build time.
65
+ __version__: str = _pkg_version("python-netgear-switch-library")
66
+ except PackageNotFoundError: # pragma: no cover - only hit outside an installed env
67
+ # Fallback for running with no install metadata available at all (e.g. a
68
+ # bare source checkout added straight to sys.path without an install).
69
+ # Deliberately does NOT import the hatch-vcs-generated `_version.py`: that
70
+ # file is gitignored and absent on a fresh checkout, and importing it here
71
+ # would make `mypy --strict` fail in that state (import-not-found) while
72
+ # a `# type: ignore` on the import would itself be flagged unused
73
+ # (no-redef) once the file exists after a build. Every installed/editable
74
+ # environment (dev, CI, wheel) already resolves the metadata version
75
+ # above, so this branch is unreachable there.
76
+ __version__ = "0.0.dev0+unknown"
77
+
78
+ __all__ = [ # noqa: RUF022 -- grouped by source module below, not alphabetical
79
+ "__version__",
80
+ # errors
81
+ "NetgearSwitchError",
82
+ "ConfigError",
83
+ "CredentialError",
84
+ "UnknownModelError",
85
+ "UnsupportedCapabilityError",
86
+ "WriteVerificationError",
87
+ "ProtectedPortError",
88
+ "HttpError",
89
+ "HttpAuthError",
90
+ "HttpUnexpectedPageError",
91
+ # models
92
+ "PortStatus",
93
+ "PoEStatus",
94
+ "PoEDetect",
95
+ "VLANInfo",
96
+ "VlanMode",
97
+ "IpMode",
98
+ "LLDPNeighbor",
99
+ "MacEntry",
100
+ "PortStats",
101
+ "MgmtIpConfig",
102
+ "Sensor",
103
+ "SwitchData",
104
+ "DetectedModel",
105
+ # NSDP full-device types (SyncSwitch.nsdp_device()/AsyncSwitch.nsdp_device())
106
+ "NsdpDevice",
107
+ "NsdpPortStatus",
108
+ "NsdpPortStatistics",
109
+ "NsdpVlanMembership",
110
+ "NsdpPortPvid",
111
+ "NsdpPortMirroring",
112
+ "NsdpIgmpSnooping",
113
+ "NsdpLinkSpeed",
114
+ "NsdpVlanEngine",
115
+ # registry
116
+ "Backend",
117
+ "SwitchClass",
118
+ "SwitchModel",
119
+ "MODELS",
120
+ "get_model",
121
+ # config
122
+ "SwitchConfig",
123
+ "resolve_secret",
124
+ "load_inventory",
125
+ "ensure_secure_file",
126
+ # facades
127
+ "SyncSwitch",
128
+ "AsyncSwitch",
129
+ # model detection (Task 2)
130
+ "detect_model",
131
+ "async_detect_model",
132
+ ]
@@ -0,0 +1,178 @@
1
+ """Internal backend-resolution seam shared by SyncSwitch and AsyncSwitch.
2
+
3
+ Only SNMP is wired in this slice. Model-driven dispatch lives here so the two
4
+ facades stay identical and Slices 5/6 can add NSDP/HTTP backends without
5
+ touching the public facade surface. Transport imports are function-local so
6
+ ``import netgear_switch`` never requires net-snmp binaries or pysnmp.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING
11
+
12
+ from .errors import CredentialError, UnsupportedCapabilityError
13
+ from .registry import Backend
14
+
15
+ if TYPE_CHECKING:
16
+ from .protocols.nsdp.client import AsyncNsdpWriteClient, NsdpWriteClient
17
+ from .protocols.snmp.client import (
18
+ AsyncSnmpClient,
19
+ AsyncSnmpWriteClient,
20
+ SnmpClient,
21
+ SnmpWriteClient,
22
+ )
23
+ from .registry import SwitchModel
24
+ from .transport.http.client import AsyncHttpClient, HttpClient
25
+
26
+ BACKEND_NOT_IMPLEMENTED = (
27
+ "model {key!r} has no SNMP backend; its NSDP backend is used instead. "
28
+ "(An HTTP-only capability is not implemented until Slice 6.)"
29
+ )
30
+
31
+
32
+ def require_snmp_backend(model: SwitchModel) -> None:
33
+ """Raise unless the model exposes an SNMP read backend."""
34
+ if Backend.SNMP not in model.backends:
35
+ raise UnsupportedCapabilityError(
36
+ BACKEND_NOT_IMPLEMENTED.format(key=model.key)
37
+ )
38
+
39
+
40
+ def require_mac_table(model: SwitchModel) -> None:
41
+ """Raise unless the model has a readable MAC/FDB table."""
42
+ if not model.has_mac_table:
43
+ raise UnsupportedCapabilityError(
44
+ f"model {model.key!r} has no MAC/FDB table"
45
+ )
46
+
47
+
48
+ def require_nsdp_backend(model: SwitchModel) -> None:
49
+ """Raise unless the model exposes an NSDP backend."""
50
+ if Backend.NSDP not in model.backends:
51
+ raise UnsupportedCapabilityError(
52
+ f"model {model.key!r} has no NSDP backend"
53
+ )
54
+
55
+
56
+ def _require_community(host: str, community: str | None) -> str:
57
+ if community is None:
58
+ raise CredentialError(
59
+ f"no SNMP read community configured for {host!r}"
60
+ )
61
+ return community
62
+
63
+
64
+ def build_sync_snmp_client(host: str, community: str | None) -> SnmpClient:
65
+ """Default sync SNMP client (net-snmp CLI). Imported lazily."""
66
+ from .transport.sync.snmp_netsnmp_cli import NetsnmpCliClient
67
+
68
+ return NetsnmpCliClient(host, _require_community(host, community))
69
+
70
+
71
+ def build_async_snmp_client(host: str, community: str | None) -> AsyncSnmpClient:
72
+ """Default async SNMP client (pysnmp). Imported lazily."""
73
+ from .transport.aio.snmp_pysnmp import PysnmpClient
74
+
75
+ return PysnmpClient(host, _require_community(host, community))
76
+
77
+
78
+ def _require_write_community(host: str, community: str | None) -> str:
79
+ # An empty string must be rejected too, not just None -- otherwise an
80
+ # unresolved/blank write-community spec could silently flow through to
81
+ # `snmpset -c ""` instead of raising (carry-forward review fix).
82
+ if not community:
83
+ raise CredentialError(
84
+ f"no SNMP write community configured for {host!r}"
85
+ )
86
+ return community
87
+
88
+
89
+ def build_sync_snmp_write_client(
90
+ host: str, write_community: str | None
91
+ ) -> SnmpWriteClient:
92
+ """Default sync SNMP write client (net-snmp CLI). Imported lazily."""
93
+ from .transport.sync.snmp_netsnmp_cli import NetsnmpCliClient
94
+
95
+ return NetsnmpCliClient(host, _require_write_community(host, write_community))
96
+
97
+
98
+ def build_async_snmp_write_client(
99
+ host: str, write_community: str | None
100
+ ) -> AsyncSnmpWriteClient:
101
+ """Default async SNMP write client (pysnmp). Imported lazily."""
102
+ from .transport.aio.snmp_pysnmp import PysnmpClient
103
+
104
+ return PysnmpClient(host, _require_write_community(host, write_community))
105
+
106
+
107
+ def build_sync_nsdp_client(host: str, interface: str | None) -> NsdpWriteClient:
108
+ """Default sync NSDP client (UDP). One client does read AND write, so the
109
+ return is annotated with the richer ``NsdpWriteClient`` protocol (the
110
+ concrete ``UdpNsdpClient`` satisfies it; ``NsdpWriteClient`` extends
111
+ ``NsdpClient`` so a read-only caller can still use it). Lazy import."""
112
+ from .transport.sync.nsdp_udp import UdpNsdpClient
113
+
114
+ return UdpNsdpClient(host, interface=interface)
115
+
116
+
117
+ def build_async_nsdp_client(host: str, interface: str | None) -> AsyncNsdpWriteClient:
118
+ """Default async NSDP client (asyncio UDP). Read AND write; annotated with
119
+ the richer ``AsyncNsdpWriteClient`` protocol (``AsyncUdpNsdpClient`` satisfies
120
+ it and it extends ``AsyncNsdpClient``, so ``_reader()`` can still accept it).
121
+ Lazy import."""
122
+ from .transport.aio.nsdp_udp import AsyncUdpNsdpClient
123
+
124
+ return AsyncUdpNsdpClient(host, interface=interface)
125
+
126
+
127
+ def require_http_backend(model: SwitchModel) -> None:
128
+ """Raise unless the model exposes an HTTP web-UI backend."""
129
+ if Backend.HTTP not in model.backends:
130
+ raise UnsupportedCapabilityError(
131
+ f"model {model.key!r} has no HTTP backend"
132
+ )
133
+
134
+
135
+ def http_reads_supported(model: SwitchModel) -> bool:
136
+ """True only if the model's web reads/writes are grounded (reads_verified).
137
+
138
+ The facade uses this to decide whether HTTP may join the per-op backend
139
+ fallback. UNVERIFIED-pending-capture models (gs110emx Gambit, gsm7228ps
140
+ cheetah) return False: their HTTP path is never used for read/write dispatch
141
+ (gsm7228ps stays SNMP-authoritative; HTTP is reserved for firmware/reboot).
142
+ The lazy import keeps ``import netgear_switch`` clear of the endpoints module
143
+ on the hot path (endpoints is pure, but this mirrors the other lazy builders).
144
+ """
145
+ if Backend.HTTP not in model.backends:
146
+ return False
147
+ from .protocols.http.endpoints import HTTP_SPECS
148
+
149
+ spec = HTTP_SPECS.get(model.key)
150
+ return spec is not None and spec.reads_verified
151
+
152
+
153
+ def _require_http_password(host: str, password: str | None) -> str:
154
+ if not password:
155
+ raise CredentialError(f"no HTTP password configured for {host!r}")
156
+ return password
157
+
158
+
159
+ def build_sync_http_client(
160
+ host: str, password: str | None, model: SwitchModel
161
+ ) -> HttpClient:
162
+ """Default sync web-UI client (httpx). Imported lazily."""
163
+ from .protocols.http.endpoints import http_spec
164
+ from .transport.http.client import HttpClient
165
+
166
+ return HttpClient(host, _require_http_password(host, password), http_spec(model))
167
+
168
+
169
+ def build_async_http_client(
170
+ host: str, password: str | None, model: SwitchModel
171
+ ) -> AsyncHttpClient:
172
+ """Default async web-UI client (httpx). Imported lazily."""
173
+ from .protocols.http.endpoints import http_spec
174
+ from .transport.http.client import AsyncHttpClient
175
+
176
+ return AsyncHttpClient(
177
+ host, _require_http_password(host, password), http_spec(model)
178
+ )
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.0.post154'
22
+ __version_tuple__ = version_tuple = (0, 0, 'post154')
23
+
24
+ __commit_id__ = commit_id = None