diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index e7efbbac..00000000 --- a/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -# Zlib preset dictionaries: exact bytes matter for Adler-32 checksums -s7commplus/zlib_dicts/*.xml binary diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 34e4b064..adfa9ea8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,6 @@ repos: - id: check-symlinks - id: check-toml - id: check-xml - exclude: ^s7commplus/zlib_dicts/ - id: check-yaml - id: check-illegal-windows-names - id: check-merge-conflict diff --git a/CHANGES.md b/CHANGES.md index 37016d39..2a375703 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,55 +1,48 @@ CHANGES ======== -4.0.0 (unreleased) -------------------- - -Major release: new `s7commplus` package with S7CommPlus protocol support. - -* Return S7 item errors when pure-Python server reads target unregistered areas - or ranges outside registered memory instead of fabricating successful data. -* Echo calling and called TSAP parameters in pure-Python server connection - confirmations, and restart the receive deadline after each TPKT header. -* Correlate S7CommPlus responses by opcode, function, and sequence; discard - bounded stale replies from earlier requests, preserve interleaved - notifications, and serialize synchronous wire requests. -* Decode corroborating CPU execution attributes so S7CommPlus `get_cpu_state()` - distinguishes RUN from STOP on S7-1500 and returns UNKNOWN for absent or - inconsistent state attributes, including S7-1200 responses that omit them. -* Support LOGO reconnection/heartbeat options and preserve explicit TSAPs. -* Correct legacy GetVarSubStreamed qualifiers and reject unusable authentication challenges. -* Validate batched symbolic read item coverage and preserve explicit PLC errors. -* Fix classic S7 TPKT bounds and COTP Class 0 header validation in sync/async clients. -* Reject incomplete reads and mismatched or missing read/write acknowledgements. -* Size read/write chunks in whole elements and preserve ctypes write datatypes; - correct BIT lengths and timer/counter index addressing. - -* New `s7commplus` package for S7CommPlus protocol (S7-1200/1500) -* S7CommPlus V1, V2 (TLS), and V3 support for S7-1200/1500 -* S7CommPlus area read/write (M, I, Q, counters, timers) -* Require an explicit target datatype in S7CommPlus multi-write tuples; add - `datatype=` to sync/async symbolic and area writes for scalar PLC targets. -* S7CommPlus PLC start/stop via INVOKE -* S7CommPlus object browsing via EXPLORE -* S7CommPlus live symbol browsing (`client.browse()`) and datablock listing (experimental) -* Fix V1 SessionKey challenge requests being rejected by S7-1200 FW 4.2 PLCs, - consume non-fatal SystemEvents while waiting for the matching response, and - strip per-fragment V3 HMACs from browse responses (#710) -* S7CommPlus active-alarm browsing and alarm subscriptions (experimental) -* S7CommPlus symbolic data subscriptions and notification decoding (experimental) -* TIA Portal XML import for SymbolTable (`SymbolTable.from_tia_xml()`) (experimental) -* S7CommPlus CPU state reading and block transfer (upload/download) -* Correct the SessionKey emulator fingerprint encoding and reject missing or - malformed authentication structures during integration tests. -* Fix the legacy SecurityKey descriptor to identify the newly generated - session key instead of an all-zero placeholder. -* Keep SessionKey activation and public connection state pending until the PLC - accepts session setup, with complete cleanup on rejection or transport error. -* **Symbolic (LID-based) access for optimized DBs** (experimental): - `Tag.from_access_string("8A0E0001.A", "REAL")` creates a symbolic Tag; - `client.read_tag(tag)` routes to S7CommPlus LID-based access via the - PLC's symbol tree. Required for S7-1200/1500 DBs with - "Optimized block access" enabled (the TIA Portal V13+ default). +3.2.0 +----- + +Feature and robustness release for the classic S7 protocol implementation. + +### Packaging + +* Move S7CommPlus support to the standalone + [`s7commplus`](https://github.com/gijzelaerr/s7commplus) package. The `s7` + import remains available as an alias for the classic `snap7` package. + +### New features + +* Add configurable request rate limiting to synchronous and asynchronous + clients, including fixed-interval and token-bucket modes (#823). +* Add synchronous and asynchronous APIs for forcing I/O bits, cancelling + forces, and reading the PLC force table (#796, #800). +* Add synchronous and asynchronous session password set/clear APIs for + password-protected PLCs (#792, #799). +* Support LOGO reconnection, heartbeat, rate-limiting, and connection callback + options while preserving explicit TSAPs (#875). +* Add an experimental serial PPI client for S7-200 PLCs, including V-memory, + system-memory, I/O, counter, and timer access (#824). + +### Bug fixes + +* Parse structured and flat SZL 0x0011 records correctly when reading order + codes and firmware versions from S7-1500 and classic PLCs (#783, #789). +* Dispatch server callbacks and queue protocol events for client requests + instead of reporting only the server start event (#853, #856). +* Validate classic S7 TPKT bounds and COTP Class 0 headers in synchronous and + asynchronous clients (#874). +* Reject incomplete reads and mismatched or missing read/write + acknowledgements (#874). +* Size read/write chunks in whole elements and preserve ctypes write + datatypes; correct BIT lengths and timer/counter index addressing (#874). +* Validate `write_multi_vars()` inputs consistently and preserve each item's + declared datatype (#855, #874). +* Echo the calling and called TSAP values in server connection confirmations, + and restart the receive deadline after each complete TPKT header (#893). +* Return S7 item errors for reads from unregistered server areas or addresses + outside registered memory instead of returning fabricated data (#896). 3.1.2 ----- @@ -166,7 +159,6 @@ Feature and bug fix release for the pure Python S7 communication library. ### Thanks -* [@bonk-dev](https://github.com/bonk-dev) — [HarpoS7](https://github.com/bonk-dev/HarpoS7): the session authentication implementation in `s7commplus/session_auth/` is a Python port of HarpoS7 (MIT license, see `s7commplus/session_auth/LICENSE-HarpoS7`) * [@hs2bws-hash](https://github.com/hs2bws-hash) — extensive real PLC testing of Partner BSend/BRecv (#668) * [@QuakeString](https://github.com/QuakeString) — read optimizer inspiration via python-snap7-optimized fork diff --git a/README.rst b/README.rst index b88a6d92..daa956ac 100644 --- a/README.rst +++ b/README.rst @@ -49,6 +49,3 @@ Documentation Read the full documentation at `python-snap7.readthedocs.io `_. - -For native communication with S7-1200 and S7-1500 controllers without PUT/GET, -see the standalone `s7commplus `_ package. diff --git a/doc/API/client-base.rst b/doc/API/client-base.rst new file mode 100644 index 00000000..a353d69b --- /dev/null +++ b/doc/API/client-base.rst @@ -0,0 +1,9 @@ +Shared Client Internals +======================= + +Pure-computation and compatibility methods shared by ``Client`` and +``AsyncClient``. Applications normally access these methods through one of the +concrete clients. + +.. automodule:: snap7.client_base + :members: diff --git a/doc/API/client.rst b/doc/API/client.rst index 9d5c2fcb..e7777af9 100644 --- a/doc/API/client.rst +++ b/doc/API/client.rst @@ -4,15 +4,42 @@ Client The ``s7`` package implements the classic S7 protocol for S7-300/400 PLCs and PUT/GET access on S7-1200/1500. +Use ``Client`` for synchronous applications:: + + from s7 import Client + + with Client() as client: + client.connect("192.168.1.10", 0, 1) + data = client.db_read(1, 0, 4) + +Use ``AsyncClient`` when the surrounding application uses ``asyncio``:: + + import asyncio + from s7 import AsyncClient + + async def main(): + async with AsyncClient() as client: + await client.connect("192.168.1.10", 0, 1) + data = await client.db_read(1, 0, 4) + + asyncio.run(main()) + +Both clients expose the same main PLC operations, including area and DB +access, block transfer, CPU information and control, clock access, force +operations, and session passwords. Methods that perform I/O are coroutines on +``AsyncClient``. See :doc:`../advanced` for the less common operations. + s7.Client --------- .. automodule:: snap7.client :members: + :inherited-members: s7.AsyncClient -------------- .. automodule:: snap7.async_client :members: + :inherited-members: :exclude-members: AsyncISOTCPConnection diff --git a/doc/API/demo.rst b/doc/API/demo.rst new file mode 100644 index 00000000..cdf47134 --- /dev/null +++ b/doc/API/demo.rst @@ -0,0 +1,9 @@ +Demo Server +=========== + +Implementation of the optional live host-metrics demo used by ``s7 demo``. +Install ``python-snap7[demo]`` before running it. This module is intended for +examples and integration testing, not production monitoring. + +.. automodule:: snap7.demo + :members: diff --git a/doc/API/ppi.rst b/doc/API/ppi.rst new file mode 100644 index 00000000..c7c1c8e7 --- /dev/null +++ b/doc/API/ppi.rst @@ -0,0 +1,10 @@ +PPI +=== + +Serial PPI transport and client support for S7-200 PLCs. Install the optional +dependency with ``pip install "python-snap7[ppi]"`` and see :doc:`../ppi` for +setup, area semantics, and current limitations. + +.. automodule:: snap7.ppi + :members: + :show-inheritance: diff --git a/doc/API/rate-limiter.rst b/doc/API/rate-limiter.rst new file mode 100644 index 00000000..0442461f --- /dev/null +++ b/doc/API/rate-limiter.rst @@ -0,0 +1,10 @@ +Rate Limiter +============ + +The synchronous and asynchronous clients create a limiter from their +``max_requests_per_second`` and ``rate_limit_*`` constructor arguments. Most +applications should configure the client instead of constructing this class +directly. + +.. automodule:: snap7.rate_limiter + :members: diff --git a/doc/API/szl.rst b/doc/API/szl.rst new file mode 100644 index 00000000..e546ef32 --- /dev/null +++ b/doc/API/szl.rst @@ -0,0 +1,8 @@ +SZL Parsers +=========== + +Helpers for decoding System Status List records returned by +``Client.read_szl()`` and ``AsyncClient.read_szl()``. + +.. automodule:: snap7.szl + :members: diff --git a/doc/API/tags.rst b/doc/API/tags.rst index 9cbf5c7d..5f741adb 100644 --- a/doc/API/tags.rst +++ b/doc/API/tags.rst @@ -90,65 +90,6 @@ Supported types Arrays are supported for any fixed-size type via ``[count]`` suffix. -Optimized block access (S7CommPlus) ------------------------------------- - -.. warning:: - - Symbolic (LID-based) access is **experimental** and requires real PLC - testing. The wire-level implementation follows the S7CommPlusDriver - reference but has not yet been validated against hardware. - -S7-1200/1500 DBs with "Optimized block access" enabled (the default in -TIA Portal V13+) do not use fixed byte offsets. The PLC internally -relocates variables between downloads, so addresses like ``DB1.DBX0.0`` -are unreliable. - -For optimized blocks, use the low-level ``read_symbolic`` and -``write_symbolic`` methods with access paths discovered via -:meth:`~s7commplus.client.S7CommPlusClient.browse`. A typed, name-based -S7CommPlus tag API is not implemented yet; :class:`~snap7.tags.Tag` and the -``read_tag`` methods above belong to the classic ``s7.Client`` API. - -.. code-block:: python - - import struct - - from s7commplus import Client - from s7commplus.protocol import DataType - - client = Client() - client.connect("192.168.1.10", use_tls=True) - - variables = client.browse() - speed_info = next(item for item in variables if item["name"] == "Motor.Speed") - path = [int(part, 16) for part in speed_info["access_sequence"].split(".")] - - # Symbolic values are currently exposed as raw wire bytes. - raw = client.read_symbolic(path[0], path[1:]) - speed = struct.unpack(">f", raw)[0] - client.write_symbolic(path[0], path[1:], struct.pack(">f", 1500.0), datatype=DataType.REAL) - -The write datatype must match the PLC variable's ``data_type`` from browsing. -For example, an INT requires ``struct.pack(">h", value)`` and -``datatype=DataType.INT``. BLOB is not a generic scalar datatype. The -``datatype=`` keyword is supported by both sync and async symbolic and area -writes; omitting it preserves the legacy BLOB encoding. - -Multi-write requires four-element tuples with an explicit datatype. The -three-element form from development versions is rejected before sending: - -.. code-block:: python - - client.db_write_multi([ - (7, 0, struct.pack(">f", 2.0), DataType.REAL), - (7, 4, struct.pack(">H", 512), DataType.WORD), - ]) - -These DB/offset addresses must be validated for the target PLC; they are not -interchangeable with browse-derived symbolic paths. Explicit BLOB remains -available for targets that accept it, including the raw-byte emulator. - API reference ------------- diff --git a/doc/API/util.rst b/doc/API/util.rst index 6f03a82f..34986547 100644 --- a/doc/API/util.rst +++ b/doc/API/util.rst @@ -13,3 +13,12 @@ REAL, STRING, etc.): .. automodule:: snap7.util :members: + +DB layout helpers +----------------- + +``DB`` and ``Row`` map a textual DB layout specification onto a byte buffer. +They are also exported directly from ``s7`` and ``snap7``. + +.. automodule:: snap7.util.db + :members: diff --git a/doc/advanced.rst b/doc/advanced.rst new file mode 100644 index 00000000..4e981bc1 --- /dev/null +++ b/doc/advanced.rst @@ -0,0 +1,90 @@ +Advanced PLC Operations +======================= + +The client API includes PLC management operations in addition to memory reads +and writes. Availability depends on the PLC family, firmware, protection +level, and current CPU state. A PLC can reject a valid request that it does not +implement. + +The examples below use :class:`~snap7.client.Client`. The corresponding +:class:`~snap7.async_client.AsyncClient` methods have the same purpose and must +be awaited. + +Identification and diagnostics +------------------------------ + +Read common identification and status records after connecting:: + + order_code = client.get_order_code() + cpu_info = client.get_cpu_info() + cp_info = client.get_cp_info() + protection = client.get_protection() + state = client.get_cpu_state() + diagnostics = client.read_diagnostic_buffer() + +For records without a convenience method, use ``read_szl(szl_id, index)`` or +``read_szl_list()``. SZL layouts vary between PLC generations; the typed +helpers in :mod:`snap7.szl` parse the layouts used by the convenience methods. + +CPU state and clock +------------------- + +``plc_stop()``, ``plc_hot_start()``, and ``plc_cold_start()`` change CPU state. +Clock operations are provided by ``get_plc_datetime()``, +``set_plc_datetime()``, and ``set_plc_system_datetime()``. + +.. warning:: + + CPU control and clock changes affect a real controller. Confirm that the + operation is safe for the machine and process before issuing it. + +Block operations +---------------- + +Use ``list_blocks()`` and ``list_blocks_of_type()`` to enumerate blocks, +``get_block_info()`` to inspect one, ``upload()`` or ``full_upload()`` to read +one, and ``download()`` or ``delete()`` to modify the PLC. ``compress()`` and +``copy_ram_to_rom()`` expose the corresponding PLC maintenance operations. + +These methods transfer individual PLC blocks; they do not create a complete +TIA Portal project or full PLC backup. + +Session passwords +----------------- + +For classic S7 CPUs configured with a session password, authenticate after +connecting and clear the password before disconnecting:: + + client.connect("192.168.1.10", 0, 2) + client.set_session_password("secret") + try: + data = client.db_read(1, 0, 4) + finally: + client.clear_session_password() + client.disconnect() + +Forcing inputs and outputs +-------------------------- + +``force_bit()`` and ``cancel_force()`` support the ``Area.PE`` input and +``Area.PA`` output areas. ``read_force_table()`` returns active force entries +when the PLC supports SZL 0x0025:: + + from s7.type import Area + + client.force_bit(Area.PA, byte_offset=0, bit=1, value=True) + active_forces = client.read_force_table() + client.cancel_force(Area.PA, byte_offset=0, bit=1) + +.. warning:: + + A force overrides normal process behavior and can remain active after the + application exits. Track every applied force and cancel it deliberately. + +Raw protocol and compatibility operations +----------------------------------------- + +``iso_exchange_buffer()`` sends a caller-provided S7 PDU. The ``as_*`` methods +and completion callbacks preserve compatibility with the historical Snap7 API; +new asynchronous applications should normally use ``AsyncClient`` instead. +See :doc:`API/client` for the complete method reference. diff --git a/doc/cli.rst b/doc/cli.rst index 21c152d2..f0f675b4 100644 --- a/doc/cli.rst +++ b/doc/cli.rst @@ -32,6 +32,19 @@ Start an emulated S7 PLC server with default values:: Port the server will listen on (default: 1102). +demo +---- + +Start a live test server that exposes host CPU, memory, disk, and network +metrics in DB1 and writable controls in DB2. Install the ``demo`` extra first:: + + pip install "python-snap7[demo]" + s7 demo --port 10102 + s7 demo --plain + +Use ``--refresh`` to set the sampling interval in seconds. This command is a +demonstration and test tool, not a production monitoring service. + read ---- diff --git a/doc/connecting.rst b/doc/connecting.rst index 95f6d6bf..fe7a7410 100644 --- a/doc/connecting.rst +++ b/doc/connecting.rst @@ -31,24 +31,25 @@ Rack/Slot Reference * - S7-1200 - 0 - 1 - - PUT/GET access must be enabled in TIA Portal (or use S7CommPlus) + - PUT/GET access must be enabled in TIA Portal * - S7-1500 - 0 - 1 - - PUT/GET access must be enabled in TIA Portal (or use S7CommPlus) - * - S7-200 / Logo + - PUT/GET access must be enabled in TIA Portal + * - S7-200 (serial PPI) - -- - -- - - Use ``set_connection_params`` with TSAP addressing (``s7`` package) + - Use ``PPIClient`` and a serial interface; see :doc:`ppi`. + * - LOGO! / S7-200 (Ethernet) + - -- + - -- + - Use ``Logo`` or the controller's documented TSAP values. .. warning:: S7-1200 and S7-1500 PLCs ship with PUT/GET communication disabled by - default. Use ``s7commplus.Client`` to communicate via S7CommPlus, which - does not require PUT/GET to be enabled. If you need to use - ``s7.Client`` (legacy protocol), enable PUT/GET in TIA Portal - under the CPU properties. See :doc:`tia-portal-config` for step-by-step - instructions. + default. Enable PUT/GET in TIA Portal under the CPU properties before using + ``s7.Client``. See :doc:`tia-portal-config` for step-by-step instructions. S7-300 @@ -71,18 +72,8 @@ S7-400 client = Client() client.connect("192.168.1.10", 0, 3) -S7-1200 / S7-1500 (S7CommPlus) -------------------------------- - -.. code-block:: python - - from s7commplus import Client - - client = Client() - client.connect("192.168.1.10") - -S7-1200 / S7-1500 (Legacy PUT/GET) ------------------------------------- +S7-1200 / S7-1500 (PUT/GET) +---------------------------- If PUT/GET access is enabled in TIA Portal, you can also use the legacy protocol: @@ -94,160 +85,26 @@ protocol: client = Client() client.connect("192.168.1.10", 0, 1) -See :doc:`API/client` for details on TLS and password authentication. +S7-200 Serial PPI +----------------- -S7CommPlus over TLS (V2/V3, TIA Portal V17+) ---------------------------------------------- +Serial S7-200 connections use :class:`~snap7.ppi.PPIClient`, not rack/slot or +TSAP addressing:: -S7-1500 firmware ≥ V2.9 and S7-1200 firmware ≥ V4.5 negotiate -S7CommPlus V2 or V3, which transports the protocol inside a TLS 1.3 -session. Pass ``use_tls=True`` to ``connect`` to activate it: + from s7 import PPIClient -.. code-block:: python + with PPIClient("/dev/ttyUSB0", station=2, baudrate=9600) as client: + data = client.v_read(0, 4) - from s7commplus import Client +Install ``python-snap7[ppi]`` and see :doc:`ppi` for supported areas and +limitations. - client = Client() - client.connect( - "192.168.1.10", - use_tls=True, - ) - data = client.db_read(1, 0, 4) - client.disconnect() +LOGO! and Ethernet TSAP Connections +----------------------------------- -The client wraps the ISO-on-TCP socket with TLS 1.3 between the -``InitSSL`` exchange and the ``CreateObject`` request. By default the -PLC's certificate is not verified — fine for development, not fine in -production. To verify the PLC against a CA bundle, pass ``tls_ca``: - -.. code-block:: python - - client.connect( - "192.168.1.10", - use_tls=True, - tls_ca="/path/to/plc-ca.pem", - ) - -If the PLC requires mutual TLS (client-side certificate), supply -``tls_cert`` and ``tls_key`` as well. - -The ``cryptography`` package is required for TLS support. Install -with the ``s7commplus`` extra: - -.. code-block:: bash - - pip install 'python-snap7[s7commplus]' - -For a source checkout, use ``python -m pip install -e '.[s7commplus]'``. -If the legacy SessionKey authentication path cannot import its dependencies, -connection setup raises an error with these installation instructions and -releases the connection; it does not continue with an unauthenticated setup. - -.. note:: - - Older S7-1200 firmware (FW < 4.5) negotiates V1 of the S7CommPlus - protocol, which predates TLS and uses a SessionKey handshake instead. - The synchronous ``Client`` performs this handshake when ``use_tls`` is - false; it does not fall back to the classic PUT/GET protocol. V1 support - is experimental and the asynchronous client currently supports only the - TLS path. See `issue #710 - `_ for the original - V1 hardware report. - -TLS handshake rejected by the PLC (connection reset) ------------------------------------------------------- - -S7 PLCs have a minimal TLS stack that rejects ``ClientHello`` messages -containing features it does not recognise. Two common causes: - -* **Post-quantum key share (OpenSSL ≥ 3.5)** — the default - ``ClientHello`` advertises the ``X25519MLKEM768`` hybrid group whose - ~1.2 KB key share the PLC drops. -* **Modern signature algorithms** — OpenSSL advertises Ed25519, Ed448, - and RSA-PSS variants in the ``signature_algorithms`` extension. - Instead of ignoring unknown algorithms (as TLS 1.2 requires), the PLC - treats them as a fatal error and sends a TCP RST. - -CPython's ``ssl`` module exposes no API for either the -``supported_groups`` or ``signature_algorithms`` lists. The fix is to -restrict both through OpenSSL's own configuration via the -``OPENSSL_CONF`` environment variable. - -1. Create an OpenSSL configuration file, e.g. ``s7-openssl.cnf``: - - .. code-block:: ini - - # Restrict TLS parameters for S7 PLC compatibility. - openssl_conf = openssl_init - - [openssl_init] - ssl_conf = ssl_configuration - - [ssl_configuration] - system_default = system_default_sect - - [system_default_sect] - # Classic ECDHE curves only — no post-quantum groups. - Groups = x25519:secp256r1:secp384r1 - # Classic signature algorithms only — no Ed25519, Ed448, or RSA-PSS. - SignatureAlgorithms = RSA+SHA256:RSA+SHA384:RSA+SHA512:ECDSA+SHA256:ECDSA+SHA384 - -2. Point ``OPENSSL_CONF`` at it **before** the Python process starts. - OpenSSL reads this configuration once, when it initialises, so - setting the variable from inside Python (e.g. via ``os.environ``) - after ``ssl`` is imported is too late — it must be set in the - environment: - - .. code-block:: bash - - # for a single run - OPENSSL_CONF=/path/to/s7-openssl.cnf python your_script.py - - # or for the whole shell session - export OPENSSL_CONF=/path/to/s7-openssl.cnf - -With both settings applied, the ``ClientHello`` contains only -algorithms that S7 PLCs understand and the handshake completes -normally. - -PLC Password Authentication ----------------------------- - -If the PLC has a password configured (``Full access (no protection)`` -disabled in TIA Portal), pass it to the synchronous client's ``connect`` -method. The client applies it to either the legacy SessionKey flow or the -TLS legitimation flow, as appropriate for the PLC: - -.. code-block:: python - - from s7commplus import Client - - client = Client() - client.connect( - "192.168.1.10", - use_tls=True, - password="hunter2", - ) - data = client.db_read(1, 0, 4) - -For an older V1 PLC, omit ``use_tls=True``; the password is then used by the -post-SessionKey legitimation exchange. The asynchronous client currently -supports password authentication only on TLS connections, using -``await client.authenticate(...)`` after connecting. - -The explicit synchronous ``authenticate`` method is also TLS-only. It -auto-detects whether the TLS PLC uses the legacy SHA-1 challenge or the newer -AES-256-CBC challenge. For accounts with a username (TIA Portal V17+ -user-based access control), pass it explicitly: - -.. code-block:: python - - client.authenticate(password="hunter2", username="operator") - -S7-200 / Logo (TSAP Connection) --------------------------------- - -S7-200 and Logo PLCs require TSAP addressing via TSAP addressing: +For LOGO!, prefer the dedicated :class:`~snap7.logo.Logo` class. Controllers +that expose classic S7 over Ethernet with explicit TSAPs can use +``set_connection_params()`` before connecting: .. code-block:: python @@ -257,6 +114,9 @@ S7-200 and Logo PLCs require TSAP addressing via TSAP addressing: client.set_connection_params("192.168.1.10", 0x1000, 0x2000) client.connect("192.168.1.10", 0, 0) +The TSAP values are controller and project specific; the example values are +not universal defaults. + Using a Non-Standard Port -------------------------- @@ -305,3 +165,19 @@ unchanged — ``snap7`` is an alias for ``s7``: client = snap7.Client() client.connect("192.168.1.10", 0, 1) + +Asynchronous Connections +------------------------ + +``AsyncClient`` provides native ``asyncio`` I/O and serializes request/response +cycles on its connection:: + + import asyncio + from s7 import AsyncClient + + async def main(): + async with AsyncClient() as client: + await client.connect("192.168.1.10", 0, 1) + data = await client.db_read(1, 0, 4) + + asyncio.run(main()) diff --git a/doc/connection-issues.rst b/doc/connection-issues.rst index 33531a4c..fe3479f3 100644 --- a/doc/connection-issues.rst +++ b/doc/connection-issues.rst @@ -28,8 +28,7 @@ long-running applications: client = Client() client.connect("192.168.1.10", 0, 1) -For finer control over reconnection parameters, use the legacy ``s7.Client`` -directly: +Configure reconnection when constructing ``s7.Client``: .. code-block:: python @@ -64,6 +63,27 @@ The parameters: - **on_reconnect**: Callback invoked after a successful reconnection. +Request Rate Limiting +--------------------- + +Both client implementations can limit outbound requests. Fixed mode spaces +requests evenly; token-bucket mode permits a bounded burst. The default rate of +zero disables limiting:: + + from s7 import Client + + client = Client( + max_requests_per_second=10, + rate_limit_algorithm="token_bucket", + rate_limit_burst=5, + rate_limit_behavior="block", + ) + +Set ``rate_limit_behavior="raise"`` to raise +:class:`~snap7.error.S7RateLimitError` instead of waiting. Rate limiting does +not make a synchronous client thread-safe; see :doc:`thread-safety`. + + Manual Reconnection ------------------- diff --git a/doc/development.rst b/doc/development.rst index addaa71e..bcbb67dd 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -81,8 +81,6 @@ Special thanks to: * Davide Nardella for creating snap7 * Thomas Hergenhahn for his libnodave * Thomas W for his S7comm wireshark plugin -* `bonk-dev `_ for `HarpoS7 `_ — the S7CommPlus session authentication (``s7commplus/session_auth/``) is a Python port of HarpoS7 (MIT) -* Thomas W (thomas-v2) for `S7CommPlusDriver `_ — the zlib preset dictionaries and S7CommPlus protocol reference (LGPL-3.0) * `Fabian Beitler `_ * `Nikteliy `_ * `Lautaro Nahuel Dapino `_ diff --git a/doc/index.rst b/doc/index.rst index 0c9b1f7a..434d785e 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -16,6 +16,8 @@ Welcome to python-snap7's documentation! connecting reading-writing multi-variable + advanced + ppi server cli tia-portal-config @@ -43,10 +45,15 @@ Welcome to python-snap7's documentation! API/server API/partner API/logo + API/ppi API/util API/tags API/optimizer + API/rate-limiter API/log + API/szl + API/discovery + API/demo API/type API/error @@ -55,9 +62,9 @@ Welcome to python-snap7's documentation! :caption: Internals API/connection + API/client-base API/s7protocol API/datatypes - API/discovery Indices and tables diff --git a/doc/installation.rst b/doc/installation.rst index eaaccb43..2b6e210e 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -1,18 +1,46 @@ Installation ============ -python-snap7 is a pure Python package with no native dependencies. Install it -using pip:: +The core python-snap7 package is pure Python and has no runtime dependencies. +Install it with pip:: $ pip install python-snap7 -If you want to use the CLI interface for running an emulator, install it with:: - - $ pip install "python-snap7[cli]" - -That's it! No native libraries or platform-specific setup is required. This works -on any platform that supports Python 3.10+, including ARM, Alpine Linux, and other -environments where the old C library was hard to install. +Optional features +----------------- + +Install only the extras needed by your application: + +.. list-table:: + :header-rows: 1 + :widths: 20 30 50 + + * - Extra + - Install command + - Purpose + * - ``cli`` + - ``pip install "python-snap7[cli]"`` + - The ``s7`` command for reading, writing, inspecting, and serving. + * - ``demo`` + - ``pip install "python-snap7[demo]"`` + - Live demo server with host metrics and terminal display. + * - ``discovery`` + - ``pip install "python-snap7[discovery]"`` + - PROFINET DCP device discovery. + * - ``ppi`` + - ``pip install "python-snap7[ppi]"`` + - Experimental S7-200 serial PPI support via pySerial. + * - ``doc`` + - ``pip install "python-snap7[doc]"`` + - Sphinx and the theme used to build this documentation. + +Extras can be combined:: + + $ pip install "python-snap7[cli,discovery,ppi]" + +No Snap7 shared library or platform-specific binary is required. Optional +dependencies may have their own platform requirements; for example, PPI needs a +serial interface and discovery needs access to a supported network interface. Upgrading from 2.x ------------------- @@ -29,4 +57,4 @@ If you experience issues after upgrading: $ pip install "python-snap7<3" - The latest stable pre-3.0 release is version 2.1.0. + The latest stable pre-3.0 release is version 2.1.1. diff --git a/doc/introduction.rst b/doc/introduction.rst index e9625931..dcb5298e 100644 --- a/doc/introduction.rst +++ b/doc/introduction.rst @@ -1,30 +1,33 @@ Introduction ============ -python-snap7 is a pure Python S7 communication library for interfacing -natively with Siemens S7 PLCs. The library implements the complete S7 -protocol stack including TPKT (RFC 1006), COTP (ISO 8073), and S7 -protocol layers. +python-snap7 is a pure Python implementation of the classic Siemens S7 +protocol. It includes the TPKT (RFC 1006), COTP (ISO 8073), and S7 protocol +layers, synchronous and asynchronous clients, and a server for testing. The name "python-snap7" is historical: the library originally started as a Python wrapper around the `Snap7 `_ C library. As of version 3.0, the C library is no longer used, but the name is kept for backwards compatibility. -python-snap7 requires Python 3.10+ and runs on Windows, macOS and Linux -without any native dependencies. +The core package requires Python 3.10+ and has no runtime dependencies. It runs +on Windows, macOS, and Linux. Optional features such as serial PPI, PROFINET +discovery, and the command-line tools have separate extras; see +:doc:`installation`. -S7CommPlus support ------------------- +Choosing an interface +--------------------- -Native S7CommPlus support for S7-1200 and S7-1500 PLCs is provided by the -standalone `s7commplus `_ package. +Use :class:`s7.Client ` or +:class:`s7.AsyncClient ` for S7-300/400 PLCs +and for S7-1200/1500 PLCs configured for PUT/GET. Use +:class:`s7.PPIClient ` for experimental serial access to +S7-200 PLCs, and :class:`s7.Logo ` for LOGO! controllers. -The ``s7`` package (legacy S7) ------------------------------- +The ``s7`` and ``snap7`` package names +-------------------------------------- -The ``s7`` package implements the classic S7 protocol. It supports -S7-300, S7-400, S7-1200 and S7-1500 PLCs via the PUT/GET interface: +``s7`` is the recommended import name: .. code-block:: python @@ -35,10 +38,6 @@ S7-300, S7-400, S7-1200 and S7-1500 PLCs via the PUT/GET interface: data = client.db_read(1, 0, 4) client.disconnect() -Use ``s7.Client`` for S7-300/400 PLCs or when PUT/GET access is enabled -on S7-1200/1500. For native S7CommPlus communication, install the standalone -``s7commplus`` package. - .. note:: The ``snap7`` package name continues to work as an alias for ``s7`` @@ -58,4 +57,9 @@ on S7-1200/1500. For native S7CommPlus communication, install the standalone clear description and the version you are using. As a workaround, you can install the last pre-3.0 release with ``pip install "python-snap7<3"``. +.. note:: + + Looking for S7CommPlus support? It moved to the standalone + `s7commplus project `_. + The project development is centralized on `github `_. diff --git a/doc/limitations.rst b/doc/limitations.rst index 19f4544c..d8dc8daa 100644 --- a/doc/limitations.rst +++ b/doc/limitations.rst @@ -1,9 +1,8 @@ Protocol Limitations and FAQ ============================ -python-snap7 implements both the classic S7 protocol and S7CommPlus over -TCP/IP. The following limitations apply to the classic protocol exposed by -``s7.Client``; the native S7CommPlus client has different capabilities: +python-snap7 implements the classic S7 protocol over TCP/IP. The following +limitations apply to ``s7.Client``: .. list-table:: :header-rows: 1 @@ -13,12 +12,9 @@ TCP/IP. The following limitations apply to the classic protocol exposed by - Explanation * - Read tag/symbol names from PLC - The classic S7 protocol only addresses data by area, DB number, and byte - offset. The experimental ``s7commplus.Client.browse()`` API can read the - symbol tree from supported S7-1200/1500 PLCs. + offset; callers must provide the addresses. * - Get DB structure or layout from PLC - Classic S7 reads DBs as raw bytes, so callers must supply the layout. - S7CommPlus browsing can reconstruct type information and optimized - symbolic access paths on supported S7-1200/1500 PLCs. * - Discover PLCs on the network - The classic S7 protocol has no broadcast discovery mechanism. However, python-snap7 provides PROFINET DCP discovery via the ``s7 discover`` @@ -27,7 +23,7 @@ TCP/IP. The following limitations apply to the classic protocol exposed by * - Create PLC backups - Full project backup requires TIA Portal. python-snap7 can upload individual blocks, but this is not a complete backup. - * - S7CommPlus V4 - - python-snap7 supports S7CommPlus V1, V2, and V3 via the ``s7commplus`` - package. V4 is not yet supported. For PLCs that require V4, use OPC UA - as an alternative. + +Serial PPI has additional constraints: the current implementation supports one +PC master and one S7-200 slave, not multimaster token passing or PPI over TCP. +See :doc:`ppi`. diff --git a/doc/plc-support.rst b/doc/plc-support.rst index 91ed2740..b28a114d 100644 --- a/doc/plc-support.rst +++ b/doc/plc-support.rst @@ -1,102 +1,57 @@ PLC Support Matrix ================== -This page documents which Siemens PLC families are supported by python-snap7, -the communication protocols they use, and any configuration requirements. +python-snap7 supports PLCs that expose the classic S7 protocol. Newer +controllers require PUT/GET access to be enabled in TIA Portal. Supported PLCs -------------- .. list-table:: :header-rows: 1 - :widths: 20 10 10 10 10 15 25 + :widths: 25 15 20 40 * - PLC Family - Introduced - - S7 (classic) - - S7CommPlus V1 - - S7CommPlus V2/V3 - - python-snap7 support + - S7 support - Notes * - S7-300 - ~1994 - - Yes - - No - - No - - **Full** + - Full - Works out of the box with ``s7.Client``. * - S7-400 - ~1996 - - Yes - - No - - No - - **Full** + - Full - Works out of the box with ``s7.Client``. - * - S7-1200 (FW ≤3) + * - S7-1200 - 2009 - - Yes - - No - - No - - **Full** - - Use ``s7.Client`` with PUT/GET enabled in TIA Portal. - * - S7-1200 (FW 4+) - - ~2014 - - Yes - - Yes - - No - - **Full** - - Use ``s7commplus.Client`` (S7CommPlus V1) or ``s7.Client`` (PUT/GET). - * - S7-1500 (FW 1.x) + - PUT/GET + - Enable PUT/GET in TIA Portal. + * - S7-1500 - 2012 - - PUT/GET only - - Yes - - No - - **Full** - - Use ``s7commplus.Client`` (S7CommPlus V1) or ``s7.Client`` (PUT/GET). - * - S7-1500 (FW 2.x) - - ~2016 - - PUT/GET only - - No - - V2 - - **Full** - - Use ``s7commplus.Client`` with TLS, or ``s7.Client`` (PUT/GET). - * - S7-1500 (FW 3.x+) - - ~2022 - - PUT/GET only - - No - - V3 - - **Full** - - Use ``s7commplus.Client`` with TLS, or ``s7.Client`` (PUT/GET). + - PUT/GET + - Enable PUT/GET in TIA Portal. * - S7-1500R/H - ~2019 - - No - - No - - V2/V3 - - **Not supported** - - Redundant CPUs; no classic S7 fallback available. + - Not supported + - Redundant CPUs have no classic S7 fallback. * - ET 200SP CPU - ~2014 - - PUT/GET only - - Yes - - Yes - - **PUT/GET only** - - Same behavior as S7-1500 with matching firmware. + - PUT/GET + - Same behavior as an S7-1500 with matching firmware. * - S7-200 SMART - ~2012 - - Subset - - No - - No - - **Partial** - - Basic read/write works. Some advanced functions may not be available. + - Partial + - Basic Ethernet read/write works; advanced functions may be unavailable. + * - S7-200 (serial PPI) + - ~1994 + - Experimental + - Use :class:`~snap7.ppi.PPIClient`; see :doc:`ppi` for current limits. * - LOGO! 8 - ~2014 - - Subset - - No - - No - - **Full** + - Full - Use the :class:`~snap7.logo.Logo` class. - Enabling PUT/GET Access ----------------------- @@ -104,52 +59,19 @@ For S7-1200 and S7-1500 PLCs, classic S7 protocol access requires the **PUT/GET** option to be enabled. See :doc:`tia-portal-config` for step-by-step instructions. -When using ``s7commplus.Client``, the S7CommPlus protocol does **not** require -PUT/GET to be enabled. PUT/GET is only needed when using ``s7.Client`` -(legacy protocol). - .. warning:: PUT/GET access provides unauthenticated read/write access to PLC memory. Only enable this on networks that are properly segmented and secured. +Feature Availability +-------------------- -Protocol Overview ------------------ - -Siemens has evolved their PLC communication protocols over time: - -.. list-table:: - :header-rows: 1 - :widths: 20 15 15 50 - - * - Protocol - - Encryption - - Authentication - - Used by - * - S7 (classic) - - None - - None - - S7-300, S7-400, S7-1200, S7-1500 (PUT/GET mode) - * - S7CommPlus V1 - - None - - Challenge-response - - S7-1200 FW 4+, S7-1500 FW 1.x - * - S7CommPlus V2 - - TLS 1.3 - - Challenge-response + TLS - - S7-1500 FW 2.x - * - S7CommPlus V3 - - TLS - - Certificate-based - - S7-1500 FW 3.x+ - -python-snap7 implements the **classic S7 protocol** (via ``s7``) and -**S7CommPlus V1, V2, and V3** (via ``s7commplus``). The classic protocol -remains available on most PLC families via the PUT/GET mechanism. S7CommPlus V4 -is not yet supported; for PLCs that require it, consider using OPC UA as an -alternative. - +The matrix describes transport-level access, not a guarantee that every client +method is implemented by every CPU. Block transfer, CPU control, forcing, +passwords, diagnostic SZLs, and clock operations vary by model, firmware, and +protection level. See :doc:`advanced` and handle +:class:`~snap7.error.S7ProtocolError` when probing optional PLC capabilities. Alternatives for Unsupported PLCs --------------------------------- diff --git a/doc/ppi.rst b/doc/ppi.rst new file mode 100644 index 00000000..d3420ddb --- /dev/null +++ b/doc/ppi.rst @@ -0,0 +1,67 @@ +S7-200 Serial PPI +================= + +python-snap7 3.2 adds experimental serial PPI support for a PC master talking +to one Siemens S7-200 slave. Install the optional serial dependency first:: + + pip install "python-snap7[ppi]" + +Connect with the serial device and PLC station address:: + + from s7 import PPIClient + + with PPIClient("/dev/ttyUSB0", station=2, baudrate=9600) as client: + data = client.v_read(0, 4) + client.v_write(10, b"\x01\x02") + +On Windows, pass a port such as ``"COM3"``. The serial transport uses the PPI +8E1 format. The adapter must provide the physical interface required by the +PLC; a serial device name alone does not imply electrical compatibility. + +Memory areas +------------ + +Use :class:`~snap7.ppi.PPIArea` with ``read_area()`` and ``write_area()``: + +.. list-table:: + :header-rows: 1 + :widths: 20 30 50 + + * - Area + - Meaning + - Count and data units + * - ``S`` / ``SM`` + - Special memory / special markers + - Bytes + * - ``I`` / ``Q`` / ``M`` + - Inputs / outputs / markers + - Bytes + * - ``V`` + - Variable memory + - Bytes; encoded as DB1 on the wire + * - ``AI`` / ``AQ`` + - Analog inputs / outputs + - 16-bit items + * - ``C`` / ``T`` + - Counters / timers + - 16-bit items + +For example:: + + from s7 import PPIArea, PPIClient + + with PPIClient("/dev/ttyUSB0", station=2) as client: + marker_bytes = client.read_area(PPIArea.M, start=0, count=8) + counters = client.read_area(PPIArea.C, start=0, count=2) + +Limits +------ + +The initial implementation supports SD1/SD2 request framing, PDU negotiation, +bounded retries, and a single PC-master/single-PLC exchange. It does not +implement multimaster token passing or PPI over TCP. Request sizes must fit the +negotiated PDU and the PPI frame-size limit. Validate the transport against the +target hardware before relying on it in production. + +See :doc:`API/ppi` for all constructor options, frame helpers, and low-level +transport classes. diff --git a/doc/reading-writing.rst b/doc/reading-writing.rst index a57a0106..73484ce7 100644 --- a/doc/reading-writing.rst +++ b/doc/reading-writing.rst @@ -201,9 +201,9 @@ LINT (8 bytes, signed -9223372036854775808 to 9223372036854775807) value = util.get_lint(data, 0) print(f"LINT = {value}") - # Write (no set_lint helper -- use struct directly) - import struct - data = bytearray(struct.pack(">q", 123456789012345)) + # Write + data = bytearray(8) + util.set_lint(data, 0, 123456789012345) client.db_write(1, 60, data) ULINT (8 bytes, unsigned 0--18446744073709551615) @@ -218,9 +218,9 @@ ULINT (8 bytes, unsigned 0--18446744073709551615) value = util.get_ulint(data, 0) print(f"ULINT = {value}") - # Write (no set_ulint helper -- use struct directly) - import struct - data = bytearray(struct.pack(">Q", 9876543210)) + # Write + data = bytearray(8) + util.set_ulint(data, 0, 9876543210) client.db_write(1, 68, data) REAL (4 bytes, IEEE 754 float) diff --git a/pyproject.toml b/pyproject.toml index 1582e494..4a34cced 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "python-snap7" -version = "3.0.0" +version = "3.2.0" description = "Pure Python S7 communication library for Siemens PLCs" readme = "README.rst" authors = [ @@ -66,6 +66,10 @@ markers =[ ] asyncio_mode = "auto" +[[tool.uv.index]] +name = "pypi" +url = "https://pypi.org/simple" + [tool.mypy] ignore_missing_imports = true strict = true diff --git a/s7/__init__.py b/s7/__init__.py index ec91f7e1..76b0f73e 100644 --- a/s7/__init__.py +++ b/s7/__init__.py @@ -5,8 +5,7 @@ from s7 import Client # same as: from snap7 import Client from s7.type import Area # same as: from snap7.type import Area -This alias exists as a transitional step toward retiring the ``snap7`` -name. For S7CommPlus (S7-1200/1500), use ``s7commplus`` instead. +Both import names are supported; ``s7`` is recommended for new code. """ import importlib @@ -21,6 +20,7 @@ "async_client", "cli", "client", + "client_base", "connection", "datatypes", "demo", @@ -34,6 +34,7 @@ "rate_limiter", "s7protocol", "server", + "szl", "tags", "type", "util", diff --git a/snap7/__init__.py b/snap7/__init__.py index 1dbcfed8..90d8e93e 100644 --- a/snap7/__init__.py +++ b/snap7/__init__.py @@ -1,10 +1,9 @@ """ -The snap7 package (legacy). +The snap7 compatibility package. Pure Python implementation of the classic S7 protocol for communicating with Siemens S7 PLCs. This package is kept for backwards compatibility. For new -projects, use the ``s7`` package instead, which supports all PLC models and -automatically selects the best protocol (S7CommPlus or legacy S7):: +projects, use the ``s7`` package instead:: from s7 import Client diff --git a/snap7/async_client.py b/snap7/async_client.py index eff65830..7124a74c 100644 --- a/snap7/async_client.py +++ b/snap7/async_client.py @@ -1,11 +1,9 @@ -""" -Legacy async S7 client implementation. +"""Asynchronous client for the classic S7 protocol. Uses asyncio streams for non-blocking I/O with an asyncio.Lock() to serialize send/receive cycles, ensuring safe concurrent use via asyncio.gather(). -For new projects, use ``s7.AsyncClient`` instead, which supports all PLC -models and automatically selects the best protocol. +``s7.AsyncClient`` and ``snap7.AsyncClient`` expose this same implementation. """ import asyncio @@ -297,8 +295,7 @@ async def __aexit__( class AsyncClient(ClientMixin): - """ - Legacy async S7 client for classic PUT/GET communication. + """Asynchronous client for classic S7 communication. Uses asyncio streams for non-blocking I/O. An internal asyncio.Lock serializes each send+receive cycle so that concurrent coroutines diff --git a/snap7/cli.py b/snap7/cli.py index 40dec0fe..485074bd 100644 --- a/snap7/cli.py +++ b/snap7/cli.py @@ -15,9 +15,8 @@ try: import click -except ImportError: - print("CLI dependencies not installed. Try: pip install python-snap7[cli]") - raise +except ImportError as exc: + raise ImportError("CLI dependencies not installed. Install them with: pip install python-snap7[cli]") from exc from snap7 import __version__ from snap7.client import Client diff --git a/snap7/client.py b/snap7/client.py index 40f82886..cd88a8b2 100644 --- a/snap7/client.py +++ b/snap7/client.py @@ -1,9 +1,7 @@ -""" -Legacy S7 client implementation. +"""Synchronous client for the classic S7 protocol. -Pure Python implementation of the classic S7 protocol. For new projects, -use ``s7.Client`` instead, which supports all PLC models and -automatically selects the best protocol. +``s7.Client`` and ``snap7.Client`` expose this same implementation. The +``s7`` import name is recommended for new projects. """ import copy @@ -13,6 +11,7 @@ import sys import threading import time +from collections.abc import Sequence from typing import List, Any, Optional, Tuple, Union, Callable, cast from datetime import datetime from ctypes import ( @@ -270,12 +269,10 @@ def __init__(self, cache_key: tuple[int, ...], packets: list[ReadPacket], read_i class Client(ClientMixin): - """ - Legacy S7 client for classic PUT/GET communication. + """Synchronous client for classic S7 communication. Supports S7-300, S7-400, S7-1200 and S7-1500 PLCs via the classic S7 - protocol. For new projects, use ``s7.Client`` instead, which - automatically selects the best protocol for any supported PLC. + protocol. S7-1200 and S7-1500 access requires PUT/GET to be enabled. Examples: >>> from s7 import Client @@ -859,9 +856,7 @@ def read_tag(self, tag: "Union[Tag, str]", encoding: str = "latin-1") -> Any: """ resolved = Tag.from_string(tag) if isinstance(tag, str) else tag if resolved.is_symbolic: - raise NotImplementedError( - "Symbolic (LID-based) tag access requires S7CommPlus. Use s7.Client instead of snap7.Client." - ) + raise NotImplementedError("Symbolic (LID-based) tag access is not supported by the classic S7 client.") data = self.read_area(Area(resolved.area), resolved.db_number, resolved.byte_offset, resolved.size) return _decode_tag(resolved, bytearray(data), encoding=encoding) @@ -878,9 +873,7 @@ def write_tag(self, tag: "Union[Tag, str]", value: Any, encoding: str = "latin-1 """ resolved = Tag.from_string(tag) if isinstance(tag, str) else tag if resolved.is_symbolic: - raise NotImplementedError( - "Symbolic (LID-based) tag access requires S7CommPlus. Use s7.Client instead of snap7.Client." - ) + raise NotImplementedError("Symbolic (LID-based) tag access is not supported by the classic S7 client.") size = resolved.size buf = bytearray(size) # For BOOL writes, we need the current byte to preserve other bits @@ -890,14 +883,14 @@ def write_tag(self, tag: "Union[Tag, str]", value: Any, encoding: str = "latin-1 _encode_tag(resolved, buf, value, encoding=encoding) return self.write_area(Area(resolved.area), resolved.db_number, resolved.byte_offset, buf) - def read_tags(self, tags: "list[Union[Tag, str]]", encoding: str = "latin-1") -> list[Any]: + def read_tags(self, tags: "Sequence[Union[Tag, str]]", encoding: str = "latin-1") -> list[Any]: """Read multiple tags in a single optimized request. Uses the multi-variable read optimizer when available to batch reads into minimal PDU exchanges. Args: - tags: List of :class:`~snap7.tags.Tag` instances or address strings. + tags: Sequence of :class:`~snap7.tags.Tag` instances or address strings. encoding: Character encoding for STRING/FSTRING values (default ``"latin-1"``). Returns: diff --git a/snap7/server/__init__.py b/snap7/server/__init__.py index 5e41e1ff..a7d110b4 100644 --- a/snap7/server/__init__.py +++ b/snap7/server/__init__.py @@ -1,9 +1,7 @@ -""" -Legacy S7 server implementation. +"""Server implementation for the classic S7 protocol. -Provides a complete S7 server emulator for the classic S7 protocol. For new -projects, use ``s7.Server`` instead, which supports both legacy S7 and -S7CommPlus clients. +Provides a complete server emulator for the classic S7 protocol. For new +projects, use ``s7.Server`` instead. """ import logging @@ -57,8 +55,7 @@ class CPUState(IntEnum): class Server: - """ - Legacy S7 server implementation. + """Classic S7 server implementation. Emulates a Siemens S7 PLC for testing and development purposes. For new projects, use ``s7.Server`` instead. diff --git a/snap7/tags.py b/snap7/tags.py index efe1775a..ae851a08 100644 --- a/snap7/tags.py +++ b/snap7/tags.py @@ -107,7 +107,7 @@ class Tag: ``byte_offset`` and ``bit``. Supported on S7-300/400 and on S7-1200/1500 DBs with "Optimized block access" disabled. - 2. **Symbolic (LID-based) access** (S7CommPlus, for optimized DBs) — + 2. **Symbolic (LID-based) access** (for optimized DBs) — uses ``access_sequence`` (a list of LID values navigating the PLC's symbol tree) and optionally ``symbol_crc``. Required for S7-1200/1500 DBs with "Optimized block access" enabled. @@ -122,7 +122,7 @@ class Tag: bit: Bit index (0-7) for BOOL tags; 0 for others. count: Array count (1 = scalar, >1 = array). name: Optional tag name for debugging/logging. - access_sequence: LID path for S7CommPlus symbolic access (optimized DBs). + access_sequence: LID path for symbolic access to optimized DBs. symbol_crc: Symbol CRC for the PLC to validate layout version (0 = no check). """ @@ -138,7 +138,7 @@ class Tag: @property def is_symbolic(self) -> bool: - """Whether this Tag uses S7CommPlus symbolic (LID-based) access.""" + """Whether this Tag uses symbolic (LID-based) access.""" return bool(self.access_sequence) @property @@ -184,7 +184,7 @@ def from_access_string( symbol_crc: int = 0, count: int = 1, ) -> "Tag": - """Create a Tag from an S7CommPlus access string for optimized blocks. + """Create a Tag from an LID access string for optimized blocks. The access string is a dot-separated sequence of hex IDs representing the path through the PLC's symbol tree, e.g. ``"8A0E0001.A"`` (DB1, diff --git a/tests/test_logging.py b/tests/test_logging.py index 395259ba..a144e559 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -34,9 +34,9 @@ def test_update_context(self) -> None: def test_update_context_partial(self) -> None: base = logging.getLogger("test.partial") adapter = PLCLoggerAdapter(base, plc_host="1.2.3.4", rack=0, slot=1) - adapter.update_context(protocol="s7commplus") + adapter.update_context(protocol="classic-s7") assert adapter.extra is not None - assert adapter.extra.get("plc_protocol") == "s7commplus" + assert adapter.extra.get("plc_protocol") == "classic-s7" # Host/rack/slot unchanged assert adapter._prefix == "[1.2.3.4 R0/S1]" diff --git a/tests/test_server.py b/tests/test_server.py index e992e58d..49b66f83 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -5,6 +5,7 @@ from ctypes import c_char from datetime import datetime from threading import Thread +from typing import Any from unittest.mock import MagicMock, patch import pytest @@ -142,6 +143,7 @@ def test_event_queue_is_bounded(self) -> None: events = [first_event] while event := self.server.pick_event(): + assert isinstance(event, SrvEvent) events.append(event) self.assertEqual(len(events), 1024) @@ -962,7 +964,7 @@ def tearDownClass(cls) -> None: cls.server.stop() cls.server.destroy() - def _wait_for_record(self, logs, fragment: str, timeout: float = 10.0) -> None: + def _wait_for_record(self, logs: Any, fragment: str, timeout: float = 10.0) -> None: end = time.monotonic() + timeout while time.monotonic() < end: if any(fragment in record.getMessage() for record in logs.records): @@ -970,7 +972,7 @@ def _wait_for_record(self, logs, fragment: str, timeout: float = 10.0) -> None: time.sleep(0.02) self.fail(f"log containing {fragment!r} did not appear, got: {[r.getMessage() for r in logs.records]}") - def _assert_no_warnings(self, logs) -> None: + def _assert_no_warnings(self, logs: Any) -> None: warnings = [r.getMessage() for r in logs.records if r.levelno >= logging.WARNING] self.assertEqual(warnings, []) diff --git a/uv.lock b/uv.lock index 5ddc1e70..8c73ff0b 100644 --- a/uv.lock +++ b/uv.lock @@ -94,116 +94,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, ] -[[package]] -name = "cffi" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/e9/6d7724983b3d5a0908dbf74f64038ade77c18646ff6636ec7894fd392ce1/cffi-2.1.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0", size = 183837, upload-time = "2026-07-06T21:32:09.655Z" }, - { url = "https://files.pythonhosted.org/packages/69/aa/24580a278de21fd7322635556334d9b535f1cbc00b0a3919447cdf464c65/cffi-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd", size = 184226, upload-time = "2026-07-06T21:32:11.196Z" }, - { url = "https://files.pythonhosted.org/packages/88/a9/02cae418ec4beb282ace11958d9d4737793439d561fadc7e6d56f2e2b354/cffi-2.1.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46", size = 211107, upload-time = "2026-07-06T21:32:12.328Z" }, - { url = "https://files.pythonhosted.org/packages/3b/30/c806937ed5e4c2c7ac30d9d6b76b5dc57ff8b75d83800d9bb11a8253cf2a/cffi-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2", size = 218733, upload-time = "2026-07-06T21:32:13.67Z" }, - { url = "https://files.pythonhosted.org/packages/f9/cf/398272b8bbfd58aa314fda5a7f1cdbb26d1d78ae324a11211521315dd1f0/cffi-2.1.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd", size = 205543, upload-time = "2026-07-06T21:32:15.148Z" }, - { url = "https://files.pythonhosted.org/packages/45/ca/f91641185cdd90c36d317a9dc7f85e88ef8682d8b300977baff5e23c35d8/cffi-2.1.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3", size = 205460, upload-time = "2026-07-06T21:32:16.479Z" }, - { url = "https://files.pythonhosted.org/packages/38/66/04781a77b411f0bb5b234d62c1814754ab75ebe455ccff1b08e8d7aae98f/cffi-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0", size = 218760, upload-time = "2026-07-06T21:32:17.98Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9a/bb1d5ed9c3fcae158e9f6391bf309c95d98c2ac37ed56573228471d0af5e/cffi-2.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43", size = 221230, upload-time = "2026-07-06T21:32:19.407Z" }, - { url = "https://files.pythonhosted.org/packages/41/aa/3c1409cdd26094efacd1c36c66e0a6eb9d4296e4fd4f9901b8b2042f4323/cffi-2.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c", size = 213524, upload-time = "2026-07-06T21:32:20.828Z" }, - { url = "https://files.pythonhosted.org/packages/fa/75/74dfb7c3fc6ebbd408038476bd4c1d7e925c62614e7b9c534ecc34218288/cffi-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd", size = 220341, upload-time = "2026-07-06T21:32:21.9Z" }, - { url = "https://files.pythonhosted.org/packages/70/b6/9003c33a3e7d2c1306f5962e646457dcfe5a8cd8fce6bbe02d7af25db783/cffi-2.1.0-cp310-cp310-win32.whl", hash = "sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f", size = 174578, upload-time = "2026-07-06T21:32:23.073Z" }, - { url = "https://files.pythonhosted.org/packages/8a/26/710688310447531c7a22f857c7f79d9855ec18b03e04494ced723fb37e2f/cffi-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da", size = 185071, upload-time = "2026-07-06T21:32:24.671Z" }, - { url = "https://files.pythonhosted.org/packages/d3/67/85c89a59ba36a671e79638f44d466749f08179266a57e4f2ffdf92174072/cffi-2.1.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc", size = 183845, upload-time = "2026-07-06T21:32:26.32Z" }, - { url = "https://files.pythonhosted.org/packages/ea/dd/e3b0baa2d3d6a857ac72b7efbf18e32e487c9cdafcc13049ad765495b15e/cffi-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7", size = 184186, upload-time = "2026-07-06T21:32:28.025Z" }, - { url = "https://files.pythonhosted.org/packages/65/68/9f3ef890cf3c6ab97bd531c5677f67613d302165d16f8142b2811782a614/cffi-2.1.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93", size = 211892, upload-time = "2026-07-06T21:32:29.565Z" }, - { url = "https://files.pythonhosted.org/packages/22/d7/1a74539db16d8bfd839ff1515948948efbb162e574650fd3d846896eea95/cffi-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2", size = 218793, upload-time = "2026-07-06T21:32:30.951Z" }, - { url = "https://files.pythonhosted.org/packages/ec/d1/9a5b7169499e8e8d8e636de70b97ac7c9447104d2ff1a2cd94790cea5162/cffi-2.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c", size = 205737, upload-time = "2026-07-06T21:32:32.216Z" }, - { url = "https://files.pythonhosted.org/packages/ba/b0/e131a9c41f10607926278453d9596163594fe1c4ebc46efe3b5e5b34eb84/cffi-2.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f", size = 204909, upload-time = "2026-07-06T21:32:33.655Z" }, - { url = "https://files.pythonhosted.org/packages/fb/d2/4398416cd699b35167947c6e22aca52c47e69ad5695073c9f1f2c52e04aa/cffi-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565", size = 217883, upload-time = "2026-07-06T21:32:35.173Z" }, - { url = "https://files.pythonhosted.org/packages/a2/a5/d4fe77b589e5e82d43ebc809bf2e6474afe8e48e32ea050b9357645b6471/cffi-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c", size = 221251, upload-time = "2026-07-06T21:32:36.527Z" }, - { url = "https://files.pythonhosted.org/packages/22/f0/a2fc43084c0433caf7f461bccc013e28f848d04ee1c5ed7fce71423cf4d9/cffi-2.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02", size = 214250, upload-time = "2026-07-06T21:32:37.852Z" }, - { url = "https://files.pythonhosted.org/packages/04/8c/b925975448cf20634a9fbd5efceb807219db452653648d2897c0989cab2d/cffi-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e", size = 219441, upload-time = "2026-07-06T21:32:39.146Z" }, - { url = "https://files.pythonhosted.org/packages/eb/da/5c4918a2d61d86fa927d716cb3d8e4626ef8dc8f605a599d32f33897f59a/cffi-2.1.0-cp311-cp311-win32.whl", hash = "sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479", size = 174496, upload-time = "2026-07-06T21:32:40.467Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c8/6c2de1d55cf35ef8b92885d5ef280790f0fb9634d87ea1cc315176aecd61/cffi-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458", size = 185113, upload-time = "2026-07-06T21:32:41.761Z" }, - { url = "https://files.pythonhosted.org/packages/9e/4e/e8d7cb5783f1841a3c8fb3a7735838d7484d08ec08c9f984b14cac1ac0e9/cffi-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d", size = 179927, upload-time = "2026-07-06T21:32:42.961Z" }, - { url = "https://files.pythonhosted.org/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", size = 184829, upload-time = "2026-07-06T21:32:44.324Z" }, - { url = "https://files.pythonhosted.org/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", size = 184728, upload-time = "2026-07-06T21:32:45.683Z" }, - { url = "https://files.pythonhosted.org/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", size = 214815, upload-time = "2026-07-06T21:32:48.557Z" }, - { url = "https://files.pythonhosted.org/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", size = 222429, upload-time = "2026-07-06T21:32:49.848Z" }, - { url = "https://files.pythonhosted.org/packages/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", size = 210315, upload-time = "2026-07-06T21:32:51.221Z" }, - { url = "https://files.pythonhosted.org/packages/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", size = 208859, upload-time = "2026-07-06T21:32:52.512Z" }, - { url = "https://files.pythonhosted.org/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", size = 221844, upload-time = "2026-07-06T21:32:53.704Z" }, - { url = "https://files.pythonhosted.org/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", size = 225287, upload-time = "2026-07-06T21:32:54.907Z" }, - { url = "https://files.pythonhosted.org/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", size = 223681, upload-time = "2026-07-06T21:32:56.329Z" }, - { url = "https://files.pythonhosted.org/packages/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", size = 175269, upload-time = "2026-07-06T21:32:57.777Z" }, - { url = "https://files.pythonhosted.org/packages/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", size = 185881, upload-time = "2026-07-06T21:32:59.253Z" }, - { url = "https://files.pythonhosted.org/packages/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", size = 180088, upload-time = "2026-07-06T21:33:02.278Z" }, - { url = "https://files.pythonhosted.org/packages/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", size = 194331, upload-time = "2026-07-06T21:33:03.697Z" }, - { url = "https://files.pythonhosted.org/packages/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", size = 196966, upload-time = "2026-07-06T21:33:05.353Z" }, - { url = "https://files.pythonhosted.org/packages/8c/e9/45c3a76ad8d43ad9261f4c95436da61128d3ca545d72b9612c0ab5be0b1c/cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a", size = 184795, upload-time = "2026-07-06T21:33:06.699Z" }, - { url = "https://files.pythonhosted.org/packages/84/4c/82f132cb4418ee6d953d982b19191e87e2a6372c8a4ce36e50b69d6ade4a/cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea", size = 184746, upload-time = "2026-07-06T21:33:08.071Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", size = 214747, upload-time = "2026-07-06T21:33:09.671Z" }, - { url = "https://files.pythonhosted.org/packages/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", size = 222392, upload-time = "2026-07-06T21:33:10.896Z" }, - { url = "https://files.pythonhosted.org/packages/88/f6/01890cfd63c08f8eb96a8319b0443690197d240a8bd6346048cf7bde9190/cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d", size = 210285, upload-time = "2026-07-06T21:33:12.251Z" }, - { url = "https://files.pythonhosted.org/packages/a6/cf/2b684132056f438567b61e19d690dd31cd0921ace051e0a458be6074369e/cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0", size = 208801, upload-time = "2026-07-06T21:33:13.617Z" }, - { url = "https://files.pythonhosted.org/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", size = 221808, upload-time = "2026-07-06T21:33:15.466Z" }, - { url = "https://files.pythonhosted.org/packages/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", size = 225241, upload-time = "2026-07-06T21:33:16.869Z" }, - { url = "https://files.pythonhosted.org/packages/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", size = 223588, upload-time = "2026-07-06T21:33:18.239Z" }, - { url = "https://files.pythonhosted.org/packages/c6/4b/e706f67279140f92939da3475ad610df18bfd52d50f14953a8e5fede71d5/cffi-2.1.0-cp313-cp313-win32.whl", hash = "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2", size = 175248, upload-time = "2026-07-06T21:33:19.799Z" }, - { url = "https://files.pythonhosted.org/packages/5a/47/59eb7975cb0e4ef0afa764ea945b29a5bb4537a9f771cb7d6c8a5dd74c95/cffi-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512", size = 185717, upload-time = "2026-07-06T21:33:21.47Z" }, - { url = "https://files.pythonhosted.org/packages/5a/af/34fee85c48f8d94efc8597bc09470c9dd274c145f1c12e0fbc6ab6d38d74/cffi-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f", size = 180114, upload-time = "2026-07-06T21:33:22.515Z" }, - { url = "https://files.pythonhosted.org/packages/d8/f0/81478e482afa03f6d18dc8f2afb5edc45b3080853b634b5ed91961be0998/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a", size = 194142, upload-time = "2026-07-06T21:33:23.657Z" }, - { url = "https://files.pythonhosted.org/packages/7d/95/8de304305cd9204974b0ca051b86d307cafca13aa575a0ef1b44d92c0d8c/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3", size = 196819, upload-time = "2026-07-06T21:33:25.007Z" }, - { url = "https://files.pythonhosted.org/packages/20/71/7c8372d30e42415602ed9f268f7cfd66f1b855fed881ecd168bcb45dbc0b/cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d", size = 184965, upload-time = "2026-07-06T21:33:26.605Z" }, - { url = "https://files.pythonhosted.org/packages/d6/5c/584e626835f0375c928176c04137c96927165cb8733cdb3150ec04e5ee5e/cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac", size = 184952, upload-time = "2026-07-06T21:33:27.823Z" }, - { url = "https://files.pythonhosted.org/packages/2e/d2/065fcae1c73979fac8e054462478d0ff8a29c40cdc2ed7ea5676a061df53/cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6", size = 222353, upload-time = "2026-07-06T21:33:29.178Z" }, - { url = "https://files.pythonhosted.org/packages/ed/a5/e8bbb1ce5b3ac2f53ad6a10bde44318a5a8d99d4f4a000d44a6e39aeb3e4/cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913", size = 210051, upload-time = "2026-07-06T21:33:30.534Z" }, - { url = "https://files.pythonhosted.org/packages/28/ed/c127d3ac36e899c965e3361357c3befacd6578c03f40125183e41c3b219e/cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d", size = 208630, upload-time = "2026-07-06T21:33:31.753Z" }, - { url = "https://files.pythonhosted.org/packages/cc/d7/97d3136f81db489ec8d1d67748c110d6c994268fd7528014aa9f2b085e4e/cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5", size = 221593, upload-time = "2026-07-06T21:33:33.044Z" }, - { url = "https://files.pythonhosted.org/packages/d3/27/93195977168ee63aed233a1a0993a2178798654d1f4bddcdd321d6fd3b21/cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce", size = 225146, upload-time = "2026-07-06T21:33:34.224Z" }, - { url = "https://files.pythonhosted.org/packages/b3/c1/6dbd291ee2ae5a50a034aa057207081f545923bbf15dad4511e985aafff5/cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326", size = 223240, upload-time = "2026-07-06T21:33:35.57Z" }, - { url = "https://files.pythonhosted.org/packages/0f/6f/ade5ce9863a57992a6ea3d0d10d7e29b8749fc127204b3d493d667b2815f/cffi-2.1.0-cp314-cp314-win32.whl", hash = "sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd", size = 177723, upload-time = "2026-07-06T21:33:51.626Z" }, - { url = "https://files.pythonhosted.org/packages/41/de/92b9eeed4ae4a21d6fd9b2a2c8505cbed573299902ea73981cc13f7ff62c/cffi-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb", size = 187937, upload-time = "2026-07-06T21:33:53.403Z" }, - { url = "https://files.pythonhosted.org/packages/2e/1a/cc6ae6c2913a03aab8898eee57963cf1035b8df5872ed8b9115fcc7e2be8/cffi-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804", size = 183001, upload-time = "2026-07-06T21:33:54.74Z" }, - { url = "https://files.pythonhosted.org/packages/14/f0/134c00ce0779ec86dea2aa1aac69339c2741a8045072676763512363a2ea/cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714", size = 188538, upload-time = "2026-07-06T21:33:36.792Z" }, - { url = "https://files.pythonhosted.org/packages/50/d8/3b86aba791cb610d24e8a3e1b2cd529e71fa15096b04e4d4e360049d4a4c/cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376", size = 188230, upload-time = "2026-07-06T21:33:38.011Z" }, - { url = "https://files.pythonhosted.org/packages/14/d0/117dcd9209255ad8571fbc8c92ef32593a1d294dcec91ddc4e4db50606f2/cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98", size = 223899, upload-time = "2026-07-06T21:33:39.514Z" }, - { url = "https://files.pythonhosted.org/packages/b6/3d/f20f8b886b254e3ad10e15cd4186d3aed49f3e6a35ab37aab9f8f25f7c03/cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13", size = 211652, upload-time = "2026-07-06T21:33:40.851Z" }, - { url = "https://files.pythonhosted.org/packages/28/3b/fad54de07260b93ddeef4b96d0131d57ea900675df1d410ae1deee52d7a6/cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d", size = 210755, upload-time = "2026-07-06T21:33:42.183Z" }, - { url = "https://files.pythonhosted.org/packages/cc/82/3d5c705acb7abbba9bbd7d79b8e62e0f25b6120eb7ae6ac49f1b721722fe/cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056", size = 223933, upload-time = "2026-07-06T21:33:43.603Z" }, - { url = "https://files.pythonhosted.org/packages/6c/d0/47e338384ab6b1004241002fa616301020cea4fc95f283506565d252f276/cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4", size = 226749, upload-time = "2026-07-06T21:33:45.046Z" }, - { url = "https://files.pythonhosted.org/packages/70/25/65bd5b58ea4bfdfc15cde02cb5365f89ef8ab8b2adfb8fe5c4bd4233382f/cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94", size = 225703, upload-time = "2026-07-06T21:33:46.374Z" }, - { url = "https://files.pythonhosted.org/packages/dc/78/aa01ac599a8a4322533d45a1f9bc93b338276d2d59dabbe7c6d92a775c81/cffi-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76", size = 182857, upload-time = "2026-07-06T21:33:47.696Z" }, - { url = "https://files.pythonhosted.org/packages/b9/26/d00496b22de4d4228f32dde94ad996f350c8aad676d63bcca0743c8dea4d/cffi-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5", size = 194065, upload-time = "2026-07-06T21:33:48.953Z" }, - { url = "https://files.pythonhosted.org/packages/d5/dd/0c7dbf815a579ff005008a2d815a55d6bb047c349eef536d9dc53d3f0a8d/cffi-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8", size = 186404, upload-time = "2026-07-06T21:33:50.309Z" }, - { url = "https://files.pythonhosted.org/packages/55/c7/8c8c50cb11c6750051daf12164098a9a6f027ac4356967fd4d800a07f242/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c", size = 194121, upload-time = "2026-07-06T21:33:56.109Z" }, - { url = "https://files.pythonhosted.org/packages/99/e2/67680bf19a6b60d2bb7ff83baefa2a4c3d2d7dc0f3277034b802e1fc504c/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001", size = 196820, upload-time = "2026-07-06T21:33:57.288Z" }, - { url = "https://files.pythonhosted.org/packages/ed/da/4bbe583a3b3a5c8c60892124fe17f3fa3656523faf0d3484eae90f091853/cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3", size = 184936, upload-time = "2026-07-06T21:33:58.765Z" }, - { url = "https://files.pythonhosted.org/packages/e5/4b/1f4c36ab273980d7aa75bb126ea4f8971f24a96108acad3a0a084028c57b/cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc", size = 185045, upload-time = "2026-07-06T21:34:00.085Z" }, - { url = "https://files.pythonhosted.org/packages/ef/c3/ad299dc38f3583f8d916b299f028af418a9ec98bc695fcbebeae7420691c/cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699", size = 222342, upload-time = "2026-07-06T21:34:01.814Z" }, - { url = "https://files.pythonhosted.org/packages/eb/d8/df4543cc087245044ed02ef3ad8e0a26619d0075ac7a77a12dc81177851b/cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022", size = 210073, upload-time = "2026-07-06T21:34:03.255Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0e/fac738d73728c6cea2a88a2883dca54892496cbba88a1dc1f2909cb8a6f5/cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0", size = 208551, upload-time = "2026-07-06T21:34:04.433Z" }, - { url = "https://files.pythonhosted.org/packages/e6/3f/0b04a700dd64f465c93020253a793a82c9b4dff9961f48facd0df945d9b8/cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1", size = 221649, upload-time = "2026-07-06T21:34:06.157Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7c/b7379a5704c79eda57ce075869ba70a0368d1c850f803b3c0d078d39dcaf/cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28", size = 225203, upload-time = "2026-07-06T21:34:07.489Z" }, - { url = "https://files.pythonhosted.org/packages/5a/02/d5e6c43ea85c41bda2a184a3418f195fe7cf602967a8d2b94e085b83deef/cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629", size = 223263, upload-time = "2026-07-06T21:34:08.712Z" }, - { url = "https://files.pythonhosted.org/packages/2c/d8/772b8259bf75749adffb1c546828978381fb516f60cf701f6c83daf60c85/cffi-2.1.0-cp315-cp315-win32.whl", hash = "sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6", size = 177696, upload-time = "2026-07-06T21:34:26.355Z" }, - { url = "https://files.pythonhosted.org/packages/2f/dd/afa2191fc6d57fedd26e5844a2fe2fcc0bbfa00961bbaa5a41e4921e7cca/cffi-2.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853", size = 187914, upload-time = "2026-07-06T21:34:27.58Z" }, - { url = "https://files.pythonhosted.org/packages/05/ef/6cd4f8c671517162379dc79cfae5aea9106bc38abb89628d5c16adf6a838/cffi-2.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda", size = 183004, upload-time = "2026-07-06T21:34:28.905Z" }, - { url = "https://files.pythonhosted.org/packages/11/b6/12fc55092817a5faa26fb8c40c7f9d662e11a46ee248c137aafc42517d92/cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc", size = 188378, upload-time = "2026-07-06T21:34:09.926Z" }, - { url = "https://files.pythonhosted.org/packages/8d/2e/cdac88979f295fde5daa69622c7d2111e56e7ceb94f211357fbe452339e4/cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca", size = 188319, upload-time = "2026-07-06T21:34:11.101Z" }, - { url = "https://files.pythonhosted.org/packages/e0/27/1d0b408497e41a74795af122d7b603c418c5fed0171450f899afd04e594f/cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d", size = 223904, upload-time = "2026-07-06T21:34:12.606Z" }, - { url = "https://files.pythonhosted.org/packages/8b/31/e115c985105dd7ffb32444505f18ceb874bb42d992af05d5dced7ecf1980/cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8", size = 211554, upload-time = "2026-07-06T21:34:13.987Z" }, - { url = "https://files.pythonhosted.org/packages/5a/67/9e6e09409336d9e515c58367e7cfcf4f89df06ad25252675595a58eb59d5/cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd", size = 210795, upload-time = "2026-07-06T21:34:15.972Z" }, - { url = "https://files.pythonhosted.org/packages/19/e5/d3cc82a4a0be7902af279c04181ad038449c096734464a5ae1de3e1401bd/cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f", size = 223843, upload-time = "2026-07-06T21:34:17.509Z" }, - { url = "https://files.pythonhosted.org/packages/b9/65/b434abc97ce7cecc2c640fde160507c0ecc7e21544b483ba3325d2e2ea17/cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc", size = 226773, upload-time = "2026-07-06T21:34:19.05Z" }, - { url = "https://files.pythonhosted.org/packages/b5/9f/d4dc66ca651eb1145a133314cda721abf13cfac3d28c4a0402263ae6ad75/cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9", size = 225719, upload-time = "2026-07-06T21:34:20.576Z" }, - { url = "https://files.pythonhosted.org/packages/68/5a/e536c528bc8057496c360c0978559a2dc45653f89dd6151078aa7d8fca1a/cffi-2.1.0-cp315-cp315t-win32.whl", hash = "sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b", size = 182760, upload-time = "2026-07-06T21:34:22.059Z" }, - { url = "https://files.pythonhosted.org/packages/d3/0b/0ffe8b82d3875bced5fa1e7986a7a46b748262a40ab7f60b475eb9fb1bb3/cffi-2.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5", size = 193769, upload-time = "2026-07-06T21:34:23.589Z" }, - { url = "https://files.pythonhosted.org/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", size = 186405, upload-time = "2026-07-06T21:34:24.857Z" }, -] - [[package]] name = "charset-normalizer" version = "3.4.9" @@ -412,63 +302,6 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] -[[package]] -name = "cryptography" -version = "50.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bb/ad/5d6702db60b1e40b41ef513b6967ff5848f307d50f8449baf1634f5908f1/cryptography-50.0.1.tar.gz", hash = "sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20", size = 880381, upload-time = "2026-08-25T19:45:45.499Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/19/797e2aaac9df6a66f1550f49979dc1b1e39ecd2077501c30efa81e8d5d67/cryptography-50.0.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:b8f852c65863251b9e3a1b8c150ce21e59b522dbb6a7d4bc80e680d38388e986", size = 4010153, upload-time = "2026-08-25T19:44:03.155Z" }, - { url = "https://files.pythonhosted.org/packages/90/34/9ce9a62ed9dc82ca9fd6a34445b6904af56e5f38b3eae2ed32e49c36053d/cryptography-50.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f", size = 4723133, upload-time = "2026-08-25T19:44:05.461Z" }, - { url = "https://files.pythonhosted.org/packages/57/26/e6d4fc8512a51a5f9ee7bfdbfb853bce1197087df40c9ad993ad370b846f/cryptography-50.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef", size = 4712478, upload-time = "2026-08-25T19:44:07.375Z" }, - { url = "https://files.pythonhosted.org/packages/e6/de/d3cdc2815697aae84126cbd6a030ca7b6b452e28a88b501b836bd3aa7a86/cryptography-50.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8", size = 4730726, upload-time = "2026-08-25T19:44:09.294Z" }, - { url = "https://files.pythonhosted.org/packages/55/32/38c0d344b98c06d34b5df8946565a9c0d6dbf32c8e0730a7f05f0a3c6cab/cryptography-50.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45", size = 5353524, upload-time = "2026-08-25T19:44:11.96Z" }, - { url = "https://files.pythonhosted.org/packages/e1/1b/82f0f0d8858d4432be1af790477edf62aef90324041aa07c57e57bef1af7/cryptography-50.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad", size = 4746720, upload-time = "2026-08-25T19:44:14.051Z" }, - { url = "https://files.pythonhosted.org/packages/29/ba/042ca458b8c64348c768284b5d23e69b92ed53d057ab779fee628564676d/cryptography-50.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49", size = 4361866, upload-time = "2026-08-25T19:44:16.167Z" }, - { url = "https://files.pythonhosted.org/packages/39/3b/e96c1ef71edef71057c7e3c3d982ce8fda554e0c52d0cc19c18845cde3eb/cryptography-50.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f", size = 4730028, upload-time = "2026-08-25T19:44:18.085Z" }, - { url = "https://files.pythonhosted.org/packages/e3/38/45abd72ef63f2e7d0754a6cacf97bd8b69512ace7f6130d24c39ece65da2/cryptography-50.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527", size = 5308405, upload-time = "2026-08-25T19:44:20.197Z" }, - { url = "https://files.pythonhosted.org/packages/85/66/6ccca4722987ddedaa7fc9c3f4708af7431f5535666c174350830888c6b7/cryptography-50.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a", size = 4746230, upload-time = "2026-08-25T19:44:22.376Z" }, - { url = "https://files.pythonhosted.org/packages/13/0e/b1f92e013228111413f2e6743948b80bc24dfd3c1b87ba98ceea16f5df89/cryptography-50.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959", size = 4862596, upload-time = "2026-08-25T19:44:24.472Z" }, - { url = "https://files.pythonhosted.org/packages/7e/22/c3654cccc856e9d682817b04ac3ee79731cb09ca6f95996a95c904de2883/cryptography-50.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b", size = 5014082, upload-time = "2026-08-25T19:44:26.709Z" }, - { url = "https://files.pythonhosted.org/packages/42/8b/cb12b1b60c91b074ca6bf0fdd59aa8f10d8bc5f73af8faece86ef0421b37/cryptography-50.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648", size = 3842826, upload-time = "2026-08-25T19:44:28.784Z" }, - { url = "https://files.pythonhosted.org/packages/5b/f0/424cb557d99aa86ac55da5e2add02e2882e44047b6264f93ade1b975a993/cryptography-50.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30a125032e5642a21ff816e021152bd4e7e94f03eff3f4b7fca41cd22bc3110f", size = 3973525, upload-time = "2026-08-25T19:44:30.7Z" }, - { url = "https://files.pythonhosted.org/packages/4d/72/3a2711d967977ab5fc80b782837c7e8d1ac7445e764c20c381a265c57ef3/cryptography-50.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a", size = 4708817, upload-time = "2026-08-25T19:44:32.773Z" }, - { url = "https://files.pythonhosted.org/packages/b4/f2/bb1f56e10815b789df0b409a69fa4992ff3d3fef9c72747f4a6b26fed38e/cryptography-50.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367", size = 4697300, upload-time = "2026-08-25T19:44:35.144Z" }, - { url = "https://files.pythonhosted.org/packages/08/bd/ed5396be499ffcf8807a585bfe38b71a1fbdd1c342b4f9b6d0ef5162a946/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5", size = 4716039, upload-time = "2026-08-25T19:44:37.192Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6e/1cf405c5c8e8df7545378048e954792f00b7f2367af8863ce8b8f3e10607/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9", size = 5332388, upload-time = "2026-08-25T19:44:39.16Z" }, - { url = "https://files.pythonhosted.org/packages/47/92/b4317e8c32c4f47b062f5398bd79106b220a124546f42be83bf32b761e2a/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0", size = 4730293, upload-time = "2026-08-25T19:44:41.298Z" }, - { url = "https://files.pythonhosted.org/packages/39/0d/a1e7633e2c744d0f2983320a27e924ef2264c79c56e1a58d5fb0a1cfd413/cryptography-50.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc", size = 4346031, upload-time = "2026-08-25T19:44:43.245Z" }, - { url = "https://files.pythonhosted.org/packages/88/dd/b215616f9bab3fc18510c78a4e5c9f362d77838503c363dc747c7d4f5c6f/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17", size = 4715344, upload-time = "2026-08-25T19:44:45.291Z" }, - { url = "https://files.pythonhosted.org/packages/b1/1b/ec3ebd31741d0e963612c4fe43caa39341b9b1e031e469820e42e4c83918/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6", size = 5287201, upload-time = "2026-08-25T19:44:47.297Z" }, - { url = "https://files.pythonhosted.org/packages/1a/01/0127d11a762b31a9ee0221894f540318761783f3fdc4bc5d057698caebd5/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3", size = 4730023, upload-time = "2026-08-25T19:44:49.435Z" }, - { url = "https://files.pythonhosted.org/packages/9e/b9/e7425ebfb599241a0c1d7000f1b466c3062da66c19d9525031315dff7213/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6", size = 4847362, upload-time = "2026-08-25T19:44:51.94Z" }, - { url = "https://files.pythonhosted.org/packages/2d/fd/60d0ddf4defa12e482c9d5e0f554384d6e8ab25341fd15f060028fd92e6a/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149", size = 4999247, upload-time = "2026-08-25T19:44:53.876Z" }, - { url = "https://files.pythonhosted.org/packages/4d/56/bc4f2b209e766c93372cfcd59b781a0b2b59700f62a969580415b699c2b2/cryptography-50.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f74455bb086a85d5e81246412602aaa97ed095e504cd40dd261ef50be42205bf", size = 3825806, upload-time = "2026-08-25T19:44:56.209Z" }, - { url = "https://files.pythonhosted.org/packages/84/a9/ee16a903f13755e914d1eecc482fe64d1f10761c3960e5d8fa6837377aff/cryptography-50.0.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ca83d00d9e69cd5eb63f2e69c3a5a59e0cecae5ae14c6ae0b35830fe3b37bad0", size = 4035307, upload-time = "2026-08-25T19:44:58.305Z" }, - { url = "https://files.pythonhosted.org/packages/5e/a5/9ec7e81e8526c0d7a387d73386b2daed3f39e10d81a85930bd1b6bfba65c/cryptography-50.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23", size = 4751900, upload-time = "2026-08-25T19:45:00.401Z" }, - { url = "https://files.pythonhosted.org/packages/7e/3c/0e77bd5ffcf078e9dd27d3074aad6c030d9b10d0bf69329d573c927a188c/cryptography-50.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733", size = 4738357, upload-time = "2026-08-25T19:45:02.786Z" }, - { url = "https://files.pythonhosted.org/packages/27/3a/3c5f80daa4dcd47323c7af8a2fcb90de27a33564d4fcac69846c0972691a/cryptography-50.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88", size = 4758474, upload-time = "2026-08-25T19:45:04.889Z" }, - { url = "https://files.pythonhosted.org/packages/6e/2b/214cf0cf93db9628c3c20c896b229f327f6fb1b20e4b3743d8ad3f00af8b/cryptography-50.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054", size = 5375862, upload-time = "2026-08-25T19:45:07.163Z" }, - { url = "https://files.pythonhosted.org/packages/d6/51/3f9701867a46b6c1740c9b52fc4d3bed6cbdcfedcc9b6e64305c07f39cff/cryptography-50.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5", size = 4772942, upload-time = "2026-08-25T19:45:09.396Z" }, - { url = "https://files.pythonhosted.org/packages/0d/5c/13ea642e08e2544d0f5396122055f4820cfacb3203562197b5967125ea97/cryptography-50.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361", size = 4383347, upload-time = "2026-08-25T19:45:11.659Z" }, - { url = "https://files.pythonhosted.org/packages/84/d5/7d1fe1cb93f91c428093ff234e128c89ba8ea61a6f26aab406081f9b996e/cryptography-50.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71", size = 4758050, upload-time = "2026-08-25T19:45:13.745Z" }, - { url = "https://files.pythonhosted.org/packages/dd/04/557fc5ead96a829e0bc812a3b9dc4a52a2f27e4f7f5950da7ff27653a805/cryptography-50.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80", size = 5332955, upload-time = "2026-08-25T19:45:16.193Z" }, - { url = "https://files.pythonhosted.org/packages/8c/eb/5d7124083e8d8cda8f5b348f544b71ad6f707ad63193758ef4d8e569da02/cryptography-50.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239", size = 4772694, upload-time = "2026-08-25T19:45:18.315Z" }, - { url = "https://files.pythonhosted.org/packages/63/8e/f1f955e0921dd2b6d22eae7e8d24a4c4b638d10735ffbf6a71f99eb0fcb8/cryptography-50.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558", size = 4888413, upload-time = "2026-08-25T19:45:20.4Z" }, - { url = "https://files.pythonhosted.org/packages/1f/ab/89e2b798d2c3925f82e2bb72d5979f3d2f6da2dd22ef4a8cd8b70d920039/cryptography-50.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e", size = 5044355, upload-time = "2026-08-25T19:45:22.353Z" }, - { url = "https://files.pythonhosted.org/packages/99/89/87ef49ffe383ef4e147d27b7bf2088fb0b54ea409dd87b5a89442e5828a5/cryptography-50.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2", size = 3875429, upload-time = "2026-08-25T19:45:24.418Z" }, - { url = "https://files.pythonhosted.org/packages/c7/27/8d207af749c453ee17ea087340b3f2b4adef75aadd1d277b1b129bdda84e/cryptography-50.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9cb3cb952cf5a8abd50c782a98a89d71699715e802fe349704b47f2425b42a94", size = 3974350, upload-time = "2026-08-25T19:45:26.551Z" }, - { url = "https://files.pythonhosted.org/packages/14/9a/6d3a4d7852e22d657438b7bf51f66102c7d71c0e1fafeec652281d0403e5/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5fe939deeb161024a6be98229c953b6591fef1f41214497a78fe793a244c017f", size = 4698675, upload-time = "2026-08-25T19:45:28.658Z" }, - { url = "https://files.pythonhosted.org/packages/73/35/5c3717edf9e68a0550ce04e28eab493fe545eccd81742af03f6a75fe260b/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fb4b9672d389c738b175c4166e78310f8a70358886aacd9173ee03a85ffdc671", size = 4707410, upload-time = "2026-08-25T19:45:30.816Z" }, - { url = "https://files.pythonhosted.org/packages/1d/e0/e786934472e3ac4ecdecc7b129a0ca1a2a40dffdafcf2c3ea9d4397f8def/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d63ae8f6481fec907ac0f588eee8a90aefde112c633131fe540e5711ddbb5a4e", size = 4698378, upload-time = "2026-08-25T19:45:33.043Z" }, - { url = "https://files.pythonhosted.org/packages/51/cf/5b3f53a0b74d122f023476ede40ba5d3e70d5cf475f73b899740d26a4fb2/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:804728ce710890870f3aaa344b2e161172d258d768ac139d02cfd9092d0d94e6", size = 4706889, upload-time = "2026-08-25T19:45:35.086Z" }, - { url = "https://files.pythonhosted.org/packages/71/44/711e61f7d014be825ef79b285b047292d1bf893732ac1bc030a351fb517f/cryptography-50.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:693c99b49bd37d0d096e4334c10232c77248c415b98d35236094cdf96d57258b", size = 3824006, upload-time = "2026-08-25T19:45:37.281Z" }, -] - [[package]] name = "distlib" version = "0.4.3" @@ -1014,15 +847,6 @@ wheels = [ { 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" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, -] - [[package]] name = "pygments" version = "2.20.0" @@ -1141,7 +965,7 @@ wheels = [ [[package]] name = "python-snap7" -version = "3.0.0" +version = "3.2.0" source = { editable = "." } [package.optional-dependencies] @@ -1166,9 +990,6 @@ doc = [ ppi = [ { name = "pyserial" }, ] -s7commplus = [ - { name = "cryptography" }, -] test = [ { name = "hypothesis" }, { name = "mypy" }, @@ -1188,7 +1009,6 @@ test = [ requires-dist = [ { name = "click", marker = "extra == 'cli'" }, { name = "click", marker = "extra == 'demo'" }, - { name = "cryptography", marker = "extra == 's7commplus'" }, { name = "hypothesis", marker = "extra == 'test'" }, { name = "mypy", marker = "extra == 'test'" }, { name = "pnio-dcp", marker = "extra == 'discovery'" }, @@ -1209,7 +1029,7 @@ requires-dist = [ { name = "types-setuptools", marker = "extra == 'test'" }, { name = "uv", marker = "extra == 'test'" }, ] -provides-extras = ["test", "s7commplus", "cli", "demo", "doc", "discovery", "ppi"] +provides-extras = ["test", "cli", "demo", "doc", "discovery", "ppi"] [[package]] name = "requests"