diff --git a/doc/API/discovery.rst b/doc/API/discovery.rst new file mode 100644 index 00000000..184a5636 --- /dev/null +++ b/doc/API/discovery.rst @@ -0,0 +1,7 @@ +Discovery +========= + +.. automodule:: snap7.discovery + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/cli.rst b/doc/cli.rst new file mode 100644 index 00000000..21c152d2 --- /dev/null +++ b/doc/cli.rst @@ -0,0 +1,178 @@ +Command-Line Interface +====================== + +python-snap7 includes a CLI tool called ``s7`` for interacting with Siemens S7 PLCs +from the terminal. Install the CLI dependencies with:: + + pip install python-snap7[cli] + +All subcommands are available via ``s7 ``. Use ``s7 --help`` to see +available commands, or ``s7 --help`` for detailed usage. + +Common Options +-------------- + +.. option:: -v, --verbose + + Enable debug logging output. + +.. option:: --version + + Show the python-snap7 version and exit. + +server +------ + +Start an emulated S7 PLC server with default values:: + + s7 server + s7 server --port 1102 + +.. option:: -p, --port PORT + + Port the server will listen on (default: 1102). + +read +---- + +Read data from a PLC data block:: + + # Read 16 raw bytes from DB1 at offset 0 + s7 read 192.168.1.10 --db 1 --offset 0 --size 16 + + # Read a typed value + s7 read 192.168.1.10 --db 1 --offset 0 --type int + s7 read 192.168.1.10 --db 1 --offset 4 --type real + + # Read a boolean (bit 3 of byte at offset 0) + s7 read 192.168.1.10 --db 1 --offset 0 --type bool --bit 3 + +.. option:: --db DB + + DB number to read from (required). + +.. option:: --offset OFFSET + + Byte offset to start reading (required). + +.. option:: --size SIZE + + Number of bytes to read (required for ``--type bytes``). + +.. option:: --type TYPE + + Data type to read. Choices: ``bool``, ``byte``, ``int``, ``uint``, ``word``, + ``dint``, ``udint``, ``dword``, ``real``, ``lreal``, ``string``, ``bytes`` + (default: ``bytes``). + +.. option:: --bit BIT + + Bit offset within the byte (only for ``bool`` type, default: 0). + +.. option:: --rack RACK + + PLC rack number (default: 0). + +.. option:: --slot SLOT + + PLC slot number (default: 1). + +.. option:: --port PORT + + PLC TCP port (default: 102). + +write +----- + +Write data to a PLC data block:: + + # Write raw bytes (hex) + s7 write 192.168.1.10 --db 1 --offset 0 --type bytes --value "01 02 03 04" + + # Write a typed value + s7 write 192.168.1.10 --db 1 --offset 0 --type int --value 42 + s7 write 192.168.1.10 --db 1 --offset 4 --type real --value 3.14 + + # Write a boolean + s7 write 192.168.1.10 --db 1 --offset 0 --type bool --bit 3 --value true + +.. option:: --db DB + + DB number to write to (required). + +.. option:: --offset OFFSET + + Byte offset to start writing (required). + +.. option:: --type TYPE + + Data type to write (required). Same choices as ``read``. + +.. option:: --value VALUE + + Value to write (required). For ``bytes`` type, provide hex (e.g. ``"01 02 FF"``). + For ``bool``, use ``true``/``false``/``1``/``0``. + +.. option:: --bit, --rack, --slot, --port + + Same as ``read``. + +dump +---- + +Dump the contents of a data block as a hex dump:: + + s7 dump 192.168.1.10 --db 1 + s7 dump 192.168.1.10 --db 1 --size 512 --format hex + +.. option:: --db DB + + DB number to dump (required). + +.. option:: --size SIZE + + Number of bytes to dump (default: 256). + +.. option:: --format FORMAT + + Output format: ``hex`` (default) or ``bytes`` (raw hex string). + +.. option:: --rack, --slot, --port + + Same as ``read``. + +info +---- + +Get PLC information including CPU info, state, order code, protection level, +and block counts:: + + s7 info 192.168.1.10 + s7 info 192.168.1.10 --rack 0 --slot 2 + +.. option:: --rack, --slot, --port + + Same as ``read``. + +discover +-------- + +Discover PROFINET devices on the local network using DCP (Discovery and basic +Configuration Protocol). Requires the ``discovery`` extra:: + + pip install python-snap7[discovery] + +Usage:: + + # Discover all devices (IP is the local network interface to use) + s7 discover 192.168.1.1 + s7 discover 192.168.1.1 --timeout 10 + +.. option:: --timeout SECONDS + + How long to listen for responses (default: 5.0). + +.. note:: + + Network discovery uses raw sockets and may require elevated privileges + (root/administrator) depending on your platform. diff --git a/doc/index.rst b/doc/index.rst index cade988c..5e98a829 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -17,6 +17,7 @@ Welcome to python-snap7's documentation! reading-writing multi-variable server + cli tia-portal-config .. toctree:: @@ -49,6 +50,7 @@ Welcome to python-snap7's documentation! API/connection API/s7protocol API/datatypes + API/discovery Indices and tables diff --git a/pyproject.toml b/pyproject.toml index b6d87787..b865de58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ test = ["pytest", "pytest-asyncio", "pytest-cov", "pytest-html", "hypothesis", " s7commplus = ["cryptography"] cli = ["rich", "click" ] doc = ["sphinx", "sphinx_rtd_theme"] +discovery = ["pnio-dcp"] [tool.setuptools.package-data] snap7 = ["py.typed"] diff --git a/snap7/discovery.py b/snap7/discovery.py new file mode 100644 index 00000000..94dab8c7 --- /dev/null +++ b/snap7/discovery.py @@ -0,0 +1,137 @@ +""" +PROFINET DCP network discovery for finding Siemens PLCs. + +Uses the pnio-dcp library for the underlying DCP protocol. +Install with: pip install python-snap7[discovery] +""" + +from __future__ import annotations + +import dataclasses +import logging + +logger = logging.getLogger(__name__) + + +@dataclasses.dataclass(frozen=True) +class Device: + """A discovered PROFINET device on the network.""" + + name: str + ip: str + mac: str + netmask: str = "" + gateway: str = "" + family: str = "" + + def __str__(self) -> str: + return f"{self.name} ({self.ip}) [{self.mac}]" + + +def discover(ip: str, timeout: float = 5.0) -> list[Device]: + """Discover PROFINET devices on the network using DCP Identify All. + + Args: + ip: IP address of the local network interface to use for discovery. + timeout: How long to listen for responses in seconds (default 5.0). + + Returns: + List of discovered devices. + + Raises: + ImportError: If pnio-dcp is not installed. + NotImplementedError: If the current platform is not supported by pnio-dcp. + """ + try: + from pnio_dcp import DCP + except ImportError: + raise ImportError("pnio-dcp is required for network discovery. Install it with: pip install python-snap7[discovery]") + + dcp = DCP(ip) + dcp.identify_all_timeout = int(timeout) if timeout >= 1 else 1 + + raw_devices = dcp.identify_all(timeout=int(timeout) if timeout >= 1 else 1) + + devices = [] + for raw in raw_devices: + device = Device( + name=raw.name_of_station, + ip=raw.IP, + mac=raw.MAC, + netmask=getattr(raw, "netmask", ""), + gateway=getattr(raw, "gateway", ""), + family=getattr(raw, "family", ""), + ) + devices.append(device) + logger.debug(f"Discovered: {device}") + + logger.info(f"Discovery complete: found {len(devices)} device(s)") + return devices + + +def identify(ip: str, mac: str) -> Device: + """Identify a specific device by MAC address. + + Args: + ip: IP address of the local network interface to use. + mac: MAC address of the target device (colon-separated, e.g. "00:1b:1b:12:34:56"). + + Returns: + The identified device. + + Raises: + ImportError: If pnio-dcp is not installed. + TimeoutError: If the device does not respond. + """ + try: + from pnio_dcp import DCP, DcpTimeoutError + except ImportError: + raise ImportError("pnio-dcp is required for network discovery. Install it with: pip install python-snap7[discovery]") + + dcp = DCP(ip) + try: + raw = dcp.identify(mac) + except DcpTimeoutError: + raise TimeoutError(f"No response from device {mac}") + + return Device( + name=raw.name_of_station, + ip=raw.IP, + mac=raw.MAC, + netmask=getattr(raw, "netmask", ""), + gateway=getattr(raw, "gateway", ""), + family=getattr(raw, "family", ""), + ) + + +try: + import click + + @click.command() + @click.argument("ip") + @click.option("--timeout", type=float, default=5.0, help="Discovery timeout in seconds.") + def discover_command(ip: str, timeout: float) -> None: + """Discover PROFINET devices on the network. + + IP is the address of the local network interface to use for discovery. + Requires pnio-dcp: pip install python-snap7[discovery] + """ + try: + devices = discover(ip, timeout) + except ImportError as e: + click.echo(str(e), err=True) + raise SystemExit(1) + except NotImplementedError as e: + click.echo(f"Platform not supported: {e}", err=True) + raise SystemExit(1) + + if not devices: + click.echo("No devices found.") + return + + click.echo(f"Found {len(devices)} device(s):\n") + for device in devices: + click.echo(f" {device.name:<30s} {device.ip:<16s} {device.mac}") + +except ImportError: + pass diff --git a/tests/test_discovery.py b/tests/test_discovery.py new file mode 100644 index 00000000..827d4d9d --- /dev/null +++ b/tests/test_discovery.py @@ -0,0 +1,167 @@ +"""Tests for PROFINET DCP network discovery.""" + +import dataclasses +from unittest.mock import MagicMock, patch + +import pytest + +from snap7.discovery import Device, discover, identify + + +@pytest.mark.util +class TestDevice: + def test_device_creation(self) -> None: + device = Device(name="plc-1", ip="192.168.1.10", mac="00:1b:1b:12:34:56") + assert device.name == "plc-1" + assert device.ip == "192.168.1.10" + assert device.mac == "00:1b:1b:12:34:56" + assert device.netmask == "" + assert device.gateway == "" + + def test_device_with_all_fields(self) -> None: + device = Device( + name="plc-2", + ip="10.0.0.1", + mac="AA:BB:CC:DD:EE:FF", + netmask="255.255.255.0", + gateway="10.0.0.254", + family="S7-1500", + ) + assert device.netmask == "255.255.255.0" + assert device.gateway == "10.0.0.254" + assert device.family == "S7-1500" + + def test_device_is_frozen(self) -> None: + device = Device(name="plc-1", ip="192.168.1.10", mac="00:00:00:00:00:00") + with pytest.raises(dataclasses.FrozenInstanceError): + device.name = "changed" # type: ignore[misc] + + def test_device_str(self) -> None: + device = Device(name="plc-1", ip="192.168.1.10", mac="00:1b:1b:12:34:56") + result = str(device) + assert "plc-1" in result + assert "192.168.1.10" in result + assert "00:1b:1b:12:34:56" in result + + +@pytest.mark.util +class TestDiscover: + def test_import_error_when_pnio_dcp_not_installed(self) -> None: + with patch.dict("sys.modules", {"pnio_dcp": None}): + with pytest.raises(ImportError, match="pnio-dcp is required"): + discover("192.168.1.1") + + def test_discover_returns_devices(self) -> None: + mock_raw_device = MagicMock() + mock_raw_device.name_of_station = "plc-1" + mock_raw_device.IP = "192.168.1.10" + mock_raw_device.MAC = "00:1b:1b:12:34:56" + mock_raw_device.netmask = "255.255.255.0" + mock_raw_device.gateway = "192.168.1.1" + mock_raw_device.family = "S7-1200" + + mock_dcp_class = MagicMock() + mock_dcp_instance = MagicMock() + mock_dcp_class.return_value = mock_dcp_instance + mock_dcp_instance.identify_all.return_value = [mock_raw_device] + + mock_module = MagicMock() + mock_module.DCP = mock_dcp_class + + with patch.dict("sys.modules", {"pnio_dcp": mock_module}): + devices = discover("192.168.1.1", timeout=3.0) + + assert len(devices) == 1 + assert devices[0].name == "plc-1" + assert devices[0].ip == "192.168.1.10" + assert devices[0].mac == "00:1b:1b:12:34:56" + assert devices[0].netmask == "255.255.255.0" + + def test_discover_empty_network(self) -> None: + mock_dcp_class = MagicMock() + mock_dcp_instance = MagicMock() + mock_dcp_class.return_value = mock_dcp_instance + mock_dcp_instance.identify_all.return_value = [] + + mock_module = MagicMock() + mock_module.DCP = mock_dcp_class + + with patch.dict("sys.modules", {"pnio_dcp": mock_module}): + devices = discover("192.168.1.1") + + assert devices == [] + + def test_discover_multiple_devices(self) -> None: + raw_devices = [] + for i in range(3): + mock = MagicMock() + mock.name_of_station = f"plc-{i}" + mock.IP = f"192.168.1.{10 + i}" + mock.MAC = f"00:1b:1b:12:34:{56 + i:02X}" + mock.netmask = "255.255.255.0" + mock.gateway = "192.168.1.1" + mock.family = "S7-1500" + raw_devices.append(mock) + + mock_dcp_class = MagicMock() + mock_dcp_instance = MagicMock() + mock_dcp_class.return_value = mock_dcp_instance + mock_dcp_instance.identify_all.return_value = raw_devices + + mock_module = MagicMock() + mock_module.DCP = mock_dcp_class + + with patch.dict("sys.modules", {"pnio_dcp": mock_module}): + devices = discover("192.168.1.1") + + assert len(devices) == 3 + assert devices[0].name == "plc-0" + assert devices[2].ip == "192.168.1.12" + + +@pytest.mark.util +class TestIdentify: + def test_import_error_when_pnio_dcp_not_installed(self) -> None: + with patch.dict("sys.modules", {"pnio_dcp": None}): + with pytest.raises(ImportError, match="pnio-dcp is required"): + identify("192.168.1.1", "00:1b:1b:12:34:56") + + def test_identify_returns_device(self) -> None: + mock_raw = MagicMock() + mock_raw.name_of_station = "plc-1" + mock_raw.IP = "192.168.1.10" + mock_raw.MAC = "00:1b:1b:12:34:56" + mock_raw.netmask = "255.255.255.0" + mock_raw.gateway = "192.168.1.1" + mock_raw.family = "S7-1200" + + mock_dcp_class = MagicMock() + mock_dcp_instance = MagicMock() + mock_dcp_class.return_value = mock_dcp_instance + mock_dcp_instance.identify.return_value = mock_raw + + mock_module = MagicMock() + mock_module.DCP = mock_dcp_class + mock_module.DcpTimeoutError = type("DcpTimeoutError", (Exception,), {}) + + with patch.dict("sys.modules", {"pnio_dcp": mock_module}): + device = identify("192.168.1.1", "00:1b:1b:12:34:56") + + assert device.name == "plc-1" + assert device.ip == "192.168.1.10" + + def test_identify_timeout(self) -> None: + mock_timeout_error = type("DcpTimeoutError", (Exception,), {}) + + mock_dcp_class = MagicMock() + mock_dcp_instance = MagicMock() + mock_dcp_class.return_value = mock_dcp_instance + mock_dcp_instance.identify.side_effect = mock_timeout_error() + + mock_module = MagicMock() + mock_module.DCP = mock_dcp_class + mock_module.DcpTimeoutError = mock_timeout_error + + with patch.dict("sys.modules", {"pnio_dcp": mock_module}): + with pytest.raises(TimeoutError, match="No response"): + identify("192.168.1.1", "00:1b:1b:12:34:56") diff --git a/uv.lock b/uv.lock index 7c3b888b..4ae6a34c 100644 --- a/uv.lock +++ b/uv.lock @@ -508,6 +508,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, ] +[[package]] +name = "importlib-metadata" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7", size = 27789, upload-time = "2026-03-20T06:42:55.665Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -811,6 +823,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "pnio-dcp" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "psutil" }, + { name = "setuptools-scm" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/b7/26f8dcc07c4a46a76c4961611c7457b4522f1a584da95f690648eeaee7b1/pnio_dcp-1.2.0-py3-none-any.whl", hash = "sha256:8d7d63077838c416b3dc6e58ec3790ef422e13d8bdb38be59a2da8713e3e061a", size = 24541, upload-time = "2024-01-17T13:34:03.087Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + [[package]] name = "pycparser" version = "3.0" @@ -937,6 +990,9 @@ cli = [ { name = "click" }, { name = "rich" }, ] +discovery = [ + { name = "pnio-dcp" }, +] doc = [ { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, @@ -967,6 +1023,7 @@ requires-dist = [ { name = "cryptography", marker = "extra == 's7commplus'" }, { name = "hypothesis", marker = "extra == 'test'" }, { name = "mypy", marker = "extra == 'test'" }, + { name = "pnio-dcp", marker = "extra == 'discovery'" }, { name = "pytest", marker = "extra == 'test'" }, { name = "pytest-asyncio", marker = "extra == 'test'" }, { name = "pytest-cov", marker = "extra == 'test'" }, @@ -981,7 +1038,7 @@ requires-dist = [ { name = "types-setuptools", marker = "extra == 'test'" }, { name = "uv", marker = "extra == 'test'" }, ] -provides-extras = ["test", "s7commplus", "cli", "doc"] +provides-extras = ["test", "s7commplus", "cli", "doc", "discovery"] [[package]] name = "requests" @@ -1045,6 +1102,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8f/e8/726643a3ea68c727da31570bde48c7a10f1aa60eddd628d94078fec586ff/ruff-0.15.7-py3-none-win_arm64.whl", hash = "sha256:18e8d73f1c3fdf27931497972250340f92e8c861722161a9caeb89a58ead6ed2", size = 11023304, upload-time = "2026-03-19T16:26:51.669Z" }, ] +[[package]] +name = "setuptools" +version = "82.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" }, +] + +[[package]] +name = "setuptools-scm" +version = "9.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "setuptools" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/b1/19587742aad604f1988a8a362e660e8c3ac03adccdb71c96d86526e5eb62/setuptools_scm-9.2.2.tar.gz", hash = "sha256:1c674ab4665686a0887d7e24c03ab25f24201c213e82ea689d2f3e169ef7ef57", size = 203385, upload-time = "2025-10-19T22:08:05.608Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/ea/ac2bf868899d0d2e82ef72d350d97a846110c709bacf2d968431576ca915/setuptools_scm-9.2.2-py3-none-any.whl", hash = "sha256:30e8f84d2ab1ba7cb0e653429b179395d0c33775d54807fc5f1dd6671801aef7", size = 62975, upload-time = "2025-10-19T22:08:04.007Z" }, +] + [[package]] name = "snowballstemmer" version = "3.0.1" @@ -1429,3 +1509,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/aa/92/58199fe10049f9703 wheels = [ { url = "https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.0-py3-none-any.whl", hash = "sha256:1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f", size = 5825084, upload-time = "2026-03-09T17:24:35.378Z" }, ] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +]