From 176e73e3add0f7e0122ccd83100af8906241bd67 Mon Sep 17 00:00:00 2001 From: Matt Fay Date: Thu, 9 Jan 2025 17:53:21 -0800 Subject: [PATCH] Wahdan/support array types (#17) ## Summary Support array types ### Why? It's part of the standard ### How? - Added encoders and decoders for new array types. - Reached 100% test coverage for _datatype.py module, including tests from the spec. - Updated uv lock local dev dependencies to include pytest-subtests for subtests in those unit tests, and numpy. - Updated changelog for next release. - Update type annotations to stop using Dict, List, Tuple, Type, etc. - Add codecov.yaml. - Omit protobuf files from coverage, and some untestable lines, increase coverage requirements. - Update demo notebooks. - Remove reundant notebooks. - Add documentation for unsupported datatypes. - Add just command for notebooks, remove run.sh. - Fix Dockerfile for notebooks and rename it. - Document notebooks better in readme --------- Co-authored-by: Ahmed Wahdan --- .github/CONTRIBUTING.md | 1 - CHANGELOG.md | 14 + notebook.Dockerfile => Dockerfile | 2 +- README.md | 4 +- codecov.yml | 8 + compose.yaml | 3 +- justfile | 4 + notebooks/dcmd_demo.ipynb | 2 +- ....ipynb => edge_node_and_device_demo.ipynb} | 96 +- notebooks/edge_node_demo.ipynb | 173 ---- notebooks/inspect_mqtt.ipynb | 226 ++-- notebooks/run.sh | 15 - pyproject.toml | 11 +- src/pysparkplug/_client.py | 12 +- src/pysparkplug/_config.py | 4 +- src/pysparkplug/_datatype.py | 193 +++- src/pysparkplug/_edge_node.py | 22 +- src/pysparkplug/_error.py | 6 +- src/pysparkplug/_payload.py | 14 +- src/pysparkplug/_strenum.py | 6 +- src/pysparkplug/_types.py | 16 +- test/unit_tests/test_datatype.py | 978 ++++++++++++++++++ test/unit_tests/test_metric.py | 103 ++ uv.lock | 214 +++- 24 files changed, 1731 insertions(+), 396 deletions(-) rename notebook.Dockerfile => Dockerfile (88%) create mode 100644 codecov.yml rename notebooks/{device_demo.ipynb => edge_node_and_device_demo.ipynb} (63%) delete mode 100644 notebooks/edge_node_demo.ipynb delete mode 100755 notebooks/run.sh create mode 100644 test/unit_tests/test_datatype.py create mode 100644 test/unit_tests/test_metric.py diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 2f12be8..e92dca0 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -150,7 +150,6 @@ When naming a branch, please use the syntax `username/branch-name-here`. If you - Metadata - Properties - DataSet types - - Array types - MQTT v5 - Historian/analytics (just listens) - Refactor all of `_payload.py`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 3feaa1b..876704c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,20 @@ All notable changes for `pysparkplug` will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/) and [Keep a Changelog](http://keepachangelog.com/). +## 0.5.0 (2025-01-09) + +### Added +- Support for array datatypes, i.e. INT8_ARRAY, INT16_ARRAY, INT32_ARRAY, INT64_ARRAY, +UINT8_ARRAY, UINT16_ARRAY, UINT32_ARRAY, UINT64_ARRAY, FLOAT_ARRAY, DOUBLE_ARRAY, +STRING_ARRAY, BOOLEAN_ARRAY, and DATETIME_ARRAY. + +### Changed +- Payload `metrics` attribute is now type annotated and implemented as a `tuple`. +- `DATETIME` datatypes are no longer all treated as UTC, instead properly converting +them to the UTC timezone. Naive datetime objects are thus treated as the local +timezone. +- Unsupported datatypes now raise a `NotImplementedError` when attempting to encode/decode them instead of a `ValueError`. + ## 0.4.0 (2024-10-24) ### Added diff --git a/notebook.Dockerfile b/Dockerfile similarity index 88% rename from notebook.Dockerfile rename to Dockerfile index 86de41d..22c1eff 100644 --- a/notebook.Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Start from a core stack version -FROM jupyter/scipy-notebook:2024-10-21 +FROM quay.io/jupyter/scipy-notebook:2025-01-06 # Move to directory where repo will be mounted in home directory WORKDIR /home/jovyan/pysparkplug diff --git a/README.md b/README.md index 2104a37..2b1c113 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,10 @@ $ pip install pysparkplug ### Usage -More documentation to come later, but for now, you can find some example usage notebooks in the `notebooks` directory. +Simple demos of the `EdgeNode`, `Device`, and `Client` classes publishing and subscribing all supported payloads and metric datatypes can be found in the `notebooks` directory. To run them dynamically, you'll need to install Docker and run `just notebooks` before opening up your local browser to http://localhost:8888. The password is `bokchoy`. ## Features ### Fully type annotated -`pysparkplug`'s various interfaces are fully type annotated, passing [Mypy](https://mypy.readthedocs.io/en/stable/)'s static type checker. +`pysparkplug`'s various interfaces are fully type annotated. diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..37326a8 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,8 @@ +coverage: + status: + patch: + default: + target: 100% + project: + default: + target: 69% diff --git a/compose.yaml b/compose.yaml index 2e21940..8faec83 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,6 +1,6 @@ services: emqx: - image: emqx/emqx:5.0.9 + image: emqx/emqx:5.8.4 ports: - "18083:18083" # Dashboard environment: @@ -8,7 +8,6 @@ services: notebook: build: context: . - dockerfile: notebook.Dockerfile command: - "start.sh" - "jupyter" diff --git a/justfile b/justfile index 53b3f62..f73d2f6 100644 --- a/justfile +++ b/justfile @@ -51,3 +51,7 @@ draft: publish: packaging uv publish --publish-url {{publish-url}} + +notebooks: + -docker compose up + docker compose down diff --git a/notebooks/dcmd_demo.ipynb b/notebooks/dcmd_demo.ipynb index acab21e..6bac336 100644 --- a/notebooks/dcmd_demo.ipynb +++ b/notebooks/dcmd_demo.ipynb @@ -98,7 +98,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.11" + "version": "3.12.8" } }, "nbformat": 4, diff --git a/notebooks/device_demo.ipynb b/notebooks/edge_node_and_device_demo.ipynb similarity index 63% rename from notebooks/device_demo.ipynb rename to notebooks/edge_node_and_device_demo.ipynb index d709b04..7fc87f2 100644 --- a/notebooks/device_demo.ipynb +++ b/notebooks/edge_node_and_device_demo.ipynb @@ -123,6 +123,84 @@ " ),\n", " psp.Metric(\n", " timestamp=psp.get_current_timestamp(),\n", + " name=\"int8_array\",\n", + " datatype=psp.DataType.INT8_ARRAY,\n", + " value=(1, -2, 3),\n", + " ),\n", + " psp.Metric(\n", + " timestamp=psp.get_current_timestamp(),\n", + " name=\"int16_array\",\n", + " datatype=psp.DataType.INT16_ARRAY,\n", + " value=(4, -5, 6),\n", + " ),\n", + " psp.Metric(\n", + " timestamp=psp.get_current_timestamp(),\n", + " name=\"int32_array\",\n", + " datatype=psp.DataType.INT32_ARRAY,\n", + " value=(7, -8, 9),\n", + " ),\n", + " psp.Metric(\n", + " timestamp=psp.get_current_timestamp(),\n", + " name=\"int64_array\",\n", + " datatype=psp.DataType.INT64_ARRAY,\n", + " value=(10, -11, 12),\n", + " ),\n", + " psp.Metric(\n", + " timestamp=psp.get_current_timestamp(),\n", + " name=\"uint8_array\",\n", + " datatype=psp.DataType.UINT8_ARRAY,\n", + " value=(1, 2, 3),\n", + " ),\n", + " psp.Metric(\n", + " timestamp=psp.get_current_timestamp(),\n", + " name=\"uint16_array\",\n", + " datatype=psp.DataType.UINT16_ARRAY,\n", + " value=(4, 5, 6),\n", + " ),\n", + " psp.Metric(\n", + " timestamp=psp.get_current_timestamp(),\n", + " name=\"uint32_array\",\n", + " datatype=psp.DataType.UINT32_ARRAY,\n", + " value=(7, 8, 9),\n", + " ),\n", + " psp.Metric(\n", + " timestamp=psp.get_current_timestamp(),\n", + " name=\"uint64_array\",\n", + " datatype=psp.DataType.UINT64_ARRAY,\n", + " value=(10, 11, 12),\n", + " ),\n", + " psp.Metric(\n", + " timestamp=psp.get_current_timestamp(),\n", + " name=\"float_array\",\n", + " datatype=psp.DataType.FLOAT_ARRAY,\n", + " value=(1.1, -2.2, 3.3),\n", + " ),\n", + " psp.Metric(\n", + " timestamp=psp.get_current_timestamp(),\n", + " name=\"double_array\",\n", + " datatype=psp.DataType.DOUBLE_ARRAY,\n", + " value=(4.4, -5.5, 6.6),\n", + " ),\n", + " psp.Metric(\n", + " timestamp=psp.get_current_timestamp(),\n", + " name=\"boolean_array\",\n", + " datatype=psp.DataType.BOOLEAN_ARRAY,\n", + " value=(True, False, True),\n", + " ),\n", + " psp.Metric(\n", + " timestamp=psp.get_current_timestamp(),\n", + " name=\"string_array\",\n", + " datatype=psp.DataType.STRING_ARRAY,\n", + " value=(\"hello\", \"world\"),\n", + " ),\n", + " psp.Metric(\n", + " timestamp=psp.get_current_timestamp(),\n", + " name=\"datetime_array\",\n", + " datatype=psp.DataType.DATETIME_ARRAY,\n", + " value=(datetime.datetime(2024, 1, 1), datetime.datetime.now()),\n", + " ),\n", + " psp.Metric(\n", + " timestamp=psp.get_current_timestamp(),\n", " name=\"null_uint8\",\n", " datatype=psp.DataType.UINT8,\n", " ),\n", @@ -149,19 +227,11 @@ "edge_node.connect(host)\n", "time.sleep(1)\n", "edge_node.update(metrics)\n", + "time.sleep(1)\n", "edge_node.update_device(device_id, metrics)\n", - "edge_node.deregister(device_id)" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "af1372c1-0fb2-4f39-a8b2-b01f6d27ea5f", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ + "time.sleep(1)\n", + "edge_node.deregister(device_id)\n", + "time.sleep(1)\n", "edge_node.disconnect()" ] } @@ -182,7 +252,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.11" + "version": "3.12.8" } }, "nbformat": 4, diff --git a/notebooks/edge_node_demo.ipynb b/notebooks/edge_node_demo.ipynb deleted file mode 100644 index 60d76f6..0000000 --- a/notebooks/edge_node_demo.ipynb +++ /dev/null @@ -1,173 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "808f4138-328a-4bac-839f-bec4becf1edd", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "import datetime\n", - "import time\n", - "\n", - "import pysparkplug as psp\n", - "\n", - "host = \"emqx\"\n", - "group_id = \"my_group\"\n", - "edge_node_id = \"my_edge_node\"\n", - "metrics = (\n", - " psp.Metric(\n", - " timestamp=psp.get_current_timestamp(),\n", - " name=\"uint8\",\n", - " datatype=psp.DataType.UINT8,\n", - " value=1,\n", - " ),\n", - " psp.Metric(\n", - " timestamp=psp.get_current_timestamp(),\n", - " name=\"uint16\",\n", - " datatype=psp.DataType.UINT16,\n", - " value=2,\n", - " ),\n", - " psp.Metric(\n", - " timestamp=psp.get_current_timestamp(),\n", - " name=\"uint32\",\n", - " datatype=psp.DataType.UINT32,\n", - " value=3,\n", - " ),\n", - " psp.Metric(\n", - " timestamp=psp.get_current_timestamp(),\n", - " name=\"uint64\",\n", - " datatype=psp.DataType.UINT64,\n", - " value=4,\n", - " ),\n", - " psp.Metric(\n", - " timestamp=psp.get_current_timestamp(),\n", - " name=\"int8\",\n", - " datatype=psp.DataType.INT8,\n", - " value=-1,\n", - " ),\n", - " psp.Metric(\n", - " timestamp=psp.get_current_timestamp(),\n", - " name=\"int16\",\n", - " datatype=psp.DataType.INT16,\n", - " value=-2,\n", - " ),\n", - " psp.Metric(\n", - " timestamp=psp.get_current_timestamp(),\n", - " name=\"int32\",\n", - " datatype=psp.DataType.INT32,\n", - " value=-3,\n", - " ),\n", - " psp.Metric(\n", - " timestamp=psp.get_current_timestamp(),\n", - " name=\"int64\",\n", - " datatype=psp.DataType.INT64,\n", - " value=-4,\n", - " ),\n", - " psp.Metric(\n", - " timestamp=psp.get_current_timestamp(),\n", - " name=\"float\",\n", - " datatype=psp.DataType.FLOAT,\n", - " value=1.1,\n", - " ),\n", - " psp.Metric(\n", - " timestamp=psp.get_current_timestamp(),\n", - " name=\"double\",\n", - " datatype=psp.DataType.DOUBLE,\n", - " value=2.2,\n", - " ),\n", - " psp.Metric(\n", - " timestamp=psp.get_current_timestamp(),\n", - " name=\"boolean\",\n", - " datatype=psp.DataType.BOOLEAN,\n", - " value=True,\n", - " ),\n", - " psp.Metric(\n", - " timestamp=psp.get_current_timestamp(),\n", - " name=\"string\",\n", - " datatype=psp.DataType.STRING,\n", - " value=\"hello world\",\n", - " ),\n", - " psp.Metric(\n", - " timestamp=psp.get_current_timestamp(),\n", - " name=\"datetime\",\n", - " datatype=psp.DataType.DATETIME,\n", - " value=datetime.datetime(1990, 9, 3, 5, 4, 3),\n", - " ),\n", - " psp.Metric(\n", - " timestamp=psp.get_current_timestamp(),\n", - " name=\"text\",\n", - " datatype=psp.DataType.TEXT,\n", - " value=\"iamatext\",\n", - " ),\n", - " psp.Metric(\n", - " timestamp=psp.get_current_timestamp(),\n", - " name=\"uuid\",\n", - " datatype=psp.DataType.UUID,\n", - " value=\"iamauuid\",\n", - " ),\n", - " psp.Metric(\n", - " timestamp=psp.get_current_timestamp(),\n", - " name=\"bytes\",\n", - " datatype=psp.DataType.BYTES,\n", - " value=b\"iamabytes\",\n", - " ),\n", - " psp.Metric(\n", - " timestamp=psp.get_current_timestamp(),\n", - " name=\"file\",\n", - " datatype=psp.DataType.FILE,\n", - " value=b\"iamafile\",\n", - " ),\n", - " psp.Metric(\n", - " timestamp=psp.get_current_timestamp(),\n", - " name=\"null_uint8\",\n", - " datatype=psp.DataType.UINT8,\n", - " ),\n", - " psp.Metric(\n", - " timestamp=psp.get_current_timestamp(),\n", - " name=\"historical_uint8\",\n", - " datatype=psp.DataType.UINT8,\n", - " value=1,\n", - " is_historical=True,\n", - " ),\n", - " psp.Metric(\n", - " timestamp=psp.get_current_timestamp(),\n", - " name=\"transient_uint8\",\n", - " datatype=psp.DataType.UINT8,\n", - " value=1,\n", - " is_transient=True,\n", - " ),\n", - ")\n", - "\n", - "edge_node = psp.EdgeNode(group_id, edge_node_id, metrics)\n", - "\n", - "edge_node.connect(host)\n", - "time.sleep(1)\n", - "edge_node.update(metrics)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.11" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/notebooks/inspect_mqtt.ipynb b/notebooks/inspect_mqtt.ipynb index d1df7f0..cb6c073 100644 --- a/notebooks/inspect_mqtt.ipynb +++ b/notebooks/inspect_mqtt.ipynb @@ -13,96 +13,146 @@ "output_type": "stream", "text": [ "From spBv1.0/my_group/NBIRTH/my_edge_node (QoS=0, retain=0):\n", - " Metric(timestamp=1692826699804, name='uint8', datatype=, value=1, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='uint16', datatype=, value=2, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='uint32', datatype=, value=3, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='uint64', datatype=, value=4, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='int8', datatype=, value=-1, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='int16', datatype=, value=-2, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='int32', datatype=, value=-3, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='int64', datatype=, value=-4, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='float', datatype=, value=1.100000023841858, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='double', datatype=, value=2.2, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='boolean', datatype=, value=True, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='string', datatype=, value='hello world', alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='datetime', datatype=, value=datetime.datetime(1990, 9, 3, 5, 4, 3, tzinfo=datetime.timezone.utc), alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='text', datatype=, value='iamatext', alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='uuid', datatype=, value='iamauuid', alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='bytes', datatype=, value=b'iamabytes', alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='file', datatype=, value=b'iamafile', alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='null_uint8', datatype=, value=None, alias=None, is_historical=False, is_transient=False, is_null=True)\n", - " Metric(timestamp=1692826699804, name='historical_uint8', datatype=, value=1, alias=None, is_historical=True, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='transient_uint8', datatype=, value=1, alias=None, is_historical=False, is_transient=True, is_null=False)\n", - " Metric(timestamp=1692826699804, name='bdSeq', datatype=, value=0, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uint8', datatype=, value=1, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uint16', datatype=, value=2, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uint32', datatype=, value=3, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uint64', datatype=, value=4, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='int8', datatype=, value=-1, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='int16', datatype=, value=-2, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='int32', datatype=, value=-3, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='int64', datatype=, value=-4, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='float', datatype=, value=1.100000023841858, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='double', datatype=, value=2.2, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='boolean', datatype=, value=True, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='string', datatype=, value='hello world', alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='datetime', datatype=, value=datetime.datetime(1990, 9, 3, 5, 4, 3, tzinfo=datetime.timezone.utc), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='text', datatype=, value='iamatext', alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uuid', datatype=, value='iamauuid', alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='bytes', datatype=, value=b'iamabytes', alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='file', datatype=, value=b'iamafile', alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='int8_array', datatype=, value=(1, -2, 3), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='int16_array', datatype=, value=(4, -5, 6), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='int32_array', datatype=, value=(7, -8, 9), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='int64_array', datatype=, value=(10, -11, 12), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uint8_array', datatype=, value=(1, 2, 3), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uint16_array', datatype=, value=(4, 5, 6), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uint32_array', datatype=, value=(7, 8, 9), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uint64_array', datatype=, value=(10, 11, 12), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='float_array', datatype=, value=(1.100000023841858, -2.200000047683716, 3.299999952316284), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='double_array', datatype=, value=(4.4, -5.5, 6.6), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='boolean_array', datatype=, value=(True, False, True), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='string_array', datatype=, value=('hello', 'world'), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='datetime_array', datatype=, value=(datetime.datetime(2024, 1, 1, 0, 0, tzinfo=datetime.timezone.utc), datetime.datetime(2025, 1, 10, 1, 26, 24, 618000, tzinfo=datetime.timezone.utc)), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='null_uint8', datatype=, value=None, alias=None, is_historical=False, is_transient=False, is_null=True)\n", + " Metric(timestamp=1736472384618, name='historical_uint8', datatype=, value=1, alias=None, is_historical=True, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='transient_uint8', datatype=, value=1, alias=None, is_historical=False, is_transient=True, is_null=False)\n", + " Metric(timestamp=1736472384619, name='bdSeq', datatype=, value=0, alias=None, is_historical=False, is_transient=False, is_null=False)\n", "From spBv1.0/my_group/DBIRTH/my_edge_node/my_device (QoS=0, retain=0):\n", - " Metric(timestamp=1692826699804, name='uint8', datatype=, value=1, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='uint16', datatype=, value=2, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='uint32', datatype=, value=3, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='uint64', datatype=, value=4, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='int8', datatype=, value=-1, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='int16', datatype=, value=-2, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='int32', datatype=, value=-3, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='int64', datatype=, value=-4, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='float', datatype=, value=1.100000023841858, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='double', datatype=, value=2.2, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='boolean', datatype=, value=True, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='string', datatype=, value='hello world', alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='datetime', datatype=, value=datetime.datetime(1990, 9, 3, 5, 4, 3, tzinfo=datetime.timezone.utc), alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='text', datatype=, value='iamatext', alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='uuid', datatype=, value='iamauuid', alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='bytes', datatype=, value=b'iamabytes', alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='file', datatype=, value=b'iamafile', alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='null_uint8', datatype=, value=None, alias=None, is_historical=False, is_transient=False, is_null=True)\n", - " Metric(timestamp=1692826699804, name='historical_uint8', datatype=, value=1, alias=None, is_historical=True, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='transient_uint8', datatype=, value=1, alias=None, is_historical=False, is_transient=True, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uint8', datatype=, value=1, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uint16', datatype=, value=2, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uint32', datatype=, value=3, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uint64', datatype=, value=4, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='int8', datatype=, value=-1, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='int16', datatype=, value=-2, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='int32', datatype=, value=-3, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='int64', datatype=, value=-4, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='float', datatype=, value=1.100000023841858, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='double', datatype=, value=2.2, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='boolean', datatype=, value=True, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='string', datatype=, value='hello world', alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='datetime', datatype=, value=datetime.datetime(1990, 9, 3, 5, 4, 3, tzinfo=datetime.timezone.utc), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='text', datatype=, value='iamatext', alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uuid', datatype=, value='iamauuid', alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='bytes', datatype=, value=b'iamabytes', alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='file', datatype=, value=b'iamafile', alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='int8_array', datatype=, value=(1, -2, 3), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='int16_array', datatype=, value=(4, -5, 6), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='int32_array', datatype=, value=(7, -8, 9), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='int64_array', datatype=, value=(10, -11, 12), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uint8_array', datatype=, value=(1, 2, 3), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uint16_array', datatype=, value=(4, 5, 6), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uint32_array', datatype=, value=(7, 8, 9), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uint64_array', datatype=, value=(10, 11, 12), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='float_array', datatype=, value=(1.100000023841858, -2.200000047683716, 3.299999952316284), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='double_array', datatype=, value=(4.4, -5.5, 6.6), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='boolean_array', datatype=, value=(True, False, True), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='string_array', datatype=, value=('hello', 'world'), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='datetime_array', datatype=, value=(datetime.datetime(2024, 1, 1, 0, 0, tzinfo=datetime.timezone.utc), datetime.datetime(2025, 1, 10, 1, 26, 24, 618000, tzinfo=datetime.timezone.utc)), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='null_uint8', datatype=, value=None, alias=None, is_historical=False, is_transient=False, is_null=True)\n", + " Metric(timestamp=1736472384618, name='historical_uint8', datatype=, value=1, alias=None, is_historical=True, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='transient_uint8', datatype=, value=1, alias=None, is_historical=False, is_transient=True, is_null=False)\n", "From spBv1.0/my_group/NDATA/my_edge_node (QoS=0, retain=0):\n", - " Metric(timestamp=1692826699804, name='uint8', datatype=, value=1, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='uint16', datatype=, value=2, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='uint32', datatype=, value=3, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='uint64', datatype=, value=4, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='int8', datatype=, value=-1, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='int16', datatype=, value=-2, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='int32', datatype=, value=-3, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='int64', datatype=, value=-4, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='float', datatype=, value=1.100000023841858, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='double', datatype=, value=2.2, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='boolean', datatype=, value=True, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='string', datatype=, value='hello world', alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='datetime', datatype=, value=datetime.datetime(1990, 9, 3, 5, 4, 3, tzinfo=datetime.timezone.utc), alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='text', datatype=, value='iamatext', alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='uuid', datatype=, value='iamauuid', alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='bytes', datatype=, value=b'iamabytes', alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='file', datatype=, value=b'iamafile', alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='null_uint8', datatype=, value=None, alias=None, is_historical=False, is_transient=False, is_null=True)\n", - " Metric(timestamp=1692826699804, name='historical_uint8', datatype=, value=1, alias=None, is_historical=True, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='transient_uint8', datatype=, value=1, alias=None, is_historical=False, is_transient=True, is_null=False)\n", - "From spBv1.0/my_group/DDATA/my_edge_node (QoS=0, retain=0):\n", - " Metric(timestamp=1692826699804, name='uint8', datatype=, value=1, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='uint16', datatype=, value=2, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='uint32', datatype=, value=3, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='uint64', datatype=, value=4, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='int8', datatype=, value=-1, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='int16', datatype=, value=-2, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='int32', datatype=, value=-3, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='int64', datatype=, value=-4, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='float', datatype=, value=1.100000023841858, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='double', datatype=, value=2.2, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='boolean', datatype=, value=True, alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='string', datatype=, value='hello world', alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='datetime', datatype=, value=datetime.datetime(1990, 9, 3, 5, 4, 3, tzinfo=datetime.timezone.utc), alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='text', datatype=, value='iamatext', alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='uuid', datatype=, value='iamauuid', alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='bytes', datatype=, value=b'iamabytes', alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='file', datatype=, value=b'iamafile', alias=None, is_historical=False, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='null_uint8', datatype=, value=None, alias=None, is_historical=False, is_transient=False, is_null=True)\n", - " Metric(timestamp=1692826699804, name='historical_uint8', datatype=, value=1, alias=None, is_historical=True, is_transient=False, is_null=False)\n", - " Metric(timestamp=1692826699804, name='transient_uint8', datatype=, value=1, alias=None, is_historical=False, is_transient=True, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uint8', datatype=, value=1, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uint16', datatype=, value=2, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uint32', datatype=, value=3, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uint64', datatype=, value=4, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='int8', datatype=, value=-1, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='int16', datatype=, value=-2, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='int32', datatype=, value=-3, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='int64', datatype=, value=-4, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='float', datatype=, value=1.100000023841858, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='double', datatype=, value=2.2, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='boolean', datatype=, value=True, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='string', datatype=, value='hello world', alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='datetime', datatype=, value=datetime.datetime(1990, 9, 3, 5, 4, 3, tzinfo=datetime.timezone.utc), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='text', datatype=, value='iamatext', alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uuid', datatype=, value='iamauuid', alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='bytes', datatype=, value=b'iamabytes', alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='file', datatype=, value=b'iamafile', alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='int8_array', datatype=, value=(1, -2, 3), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='int16_array', datatype=, value=(4, -5, 6), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='int32_array', datatype=, value=(7, -8, 9), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='int64_array', datatype=, value=(10, -11, 12), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uint8_array', datatype=, value=(1, 2, 3), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uint16_array', datatype=, value=(4, 5, 6), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uint32_array', datatype=, value=(7, 8, 9), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uint64_array', datatype=, value=(10, 11, 12), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='float_array', datatype=, value=(1.100000023841858, -2.200000047683716, 3.299999952316284), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='double_array', datatype=, value=(4.4, -5.5, 6.6), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='boolean_array', datatype=, value=(True, False, True), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='string_array', datatype=, value=('hello', 'world'), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='datetime_array', datatype=, value=(datetime.datetime(2024, 1, 1, 0, 0, tzinfo=datetime.timezone.utc), datetime.datetime(2025, 1, 10, 1, 26, 24, 618000, tzinfo=datetime.timezone.utc)), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='null_uint8', datatype=, value=None, alias=None, is_historical=False, is_transient=False, is_null=True)\n", + " Metric(timestamp=1736472384618, name='historical_uint8', datatype=, value=1, alias=None, is_historical=True, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='transient_uint8', datatype=, value=1, alias=None, is_historical=False, is_transient=True, is_null=False)\n", + "From spBv1.0/my_group/DDATA/my_edge_node/my_device (QoS=0, retain=0):\n", + " Metric(timestamp=1736472384618, name='uint8', datatype=, value=1, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uint16', datatype=, value=2, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uint32', datatype=, value=3, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uint64', datatype=, value=4, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='int8', datatype=, value=-1, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='int16', datatype=, value=-2, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='int32', datatype=, value=-3, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='int64', datatype=, value=-4, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='float', datatype=, value=1.100000023841858, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='double', datatype=, value=2.2, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='boolean', datatype=, value=True, alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='string', datatype=, value='hello world', alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='datetime', datatype=, value=datetime.datetime(1990, 9, 3, 5, 4, 3, tzinfo=datetime.timezone.utc), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='text', datatype=, value='iamatext', alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uuid', datatype=, value='iamauuid', alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='bytes', datatype=, value=b'iamabytes', alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='file', datatype=, value=b'iamafile', alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='int8_array', datatype=, value=(1, -2, 3), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='int16_array', datatype=, value=(4, -5, 6), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='int32_array', datatype=, value=(7, -8, 9), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='int64_array', datatype=, value=(10, -11, 12), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uint8_array', datatype=, value=(1, 2, 3), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uint16_array', datatype=, value=(4, 5, 6), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uint32_array', datatype=, value=(7, 8, 9), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='uint64_array', datatype=, value=(10, 11, 12), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='float_array', datatype=, value=(1.100000023841858, -2.200000047683716, 3.299999952316284), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='double_array', datatype=, value=(4.4, -5.5, 6.6), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='boolean_array', datatype=, value=(True, False, True), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='string_array', datatype=, value=('hello', 'world'), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='datetime_array', datatype=, value=(datetime.datetime(2024, 1, 1, 0, 0, tzinfo=datetime.timezone.utc), datetime.datetime(2025, 1, 10, 1, 26, 24, 618000, tzinfo=datetime.timezone.utc)), alias=None, is_historical=False, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='null_uint8', datatype=, value=None, alias=None, is_historical=False, is_transient=False, is_null=True)\n", + " Metric(timestamp=1736472384618, name='historical_uint8', datatype=, value=1, alias=None, is_historical=True, is_transient=False, is_null=False)\n", + " Metric(timestamp=1736472384618, name='transient_uint8', datatype=, value=1, alias=None, is_historical=False, is_transient=True, is_null=False)\n", "From spBv1.0/my_group/DDEATH/my_edge_node/my_device (QoS=0, retain=0):\n", - " DDeath(timestamp=1692826700815, seq=4)\n", - "From spBv1.0/my_group/DCMD/my_edge_node/my_device (QoS=0, retain=0):\n", - " DCmd(timestamp=1692827052224, metrics=(Metric(timestamp=1692827052224, name='my_metric', datatype=, value=1.1, alias=None, is_historical=False, is_transient=False, is_null=False),))\n", - "From spBv1.0/my_group/DCMD/my_edge_node/my_device (QoS=0, retain=0):\n", - " DCmd(timestamp=1692827112789, metrics=(Metric(timestamp=1692827112789, name='my_metric', datatype=, value=1.1, alias=None, is_historical=False, is_transient=False, is_null=False),))\n" + " DDeath(timestamp=1736472387626, seq=4)\n", + "From spBv1.0/my_group/NDEATH/my_edge_node (QoS=0, retain=0):\n", + " NDeath(timestamp=None, bd_seq_metric=Metric(timestamp=1736472384620, name='bdSeq', datatype=, value=1, alias=None, is_historical=False, is_transient=False, is_null=False))\n" ] } ], @@ -143,7 +193,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.11" + "version": "3.12.8" } }, "nbformat": 4, diff --git a/notebooks/run.sh b/notebooks/run.sh deleted file mode 100755 index a9e70e1..0000000 --- a/notebooks/run.sh +++ /dev/null @@ -1,15 +0,0 @@ -#! /usr/bin/env bash -set -o errexit -o nounset -o pipefail -IFS=$'\n\t' - -REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"/.. -cd "$REPO_DIR" - -echo "Opening notebook environment" - -cleanup() { - docker compose down -} -trap cleanup EXIT - -docker compose up notebook emqx diff --git a/pyproject.toml b/pyproject.toml index c46ba48..b79d3ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,10 +43,12 @@ dev = [ "coverage>=7.6.10", "furo>=2024.8.6", "myst-parser>=3.0.1", + "numpy>=2.0.2", "packaging>=24.2", "pygithub>=2.5.0", "pyright>=1.1.391", "pytest>=8.3.4", + "pytest-subtests>=0.14.1", "ruff>=0.8.5", "sphinx>=7.4.7", "sphinx-copybutton>=0.5.2", @@ -85,16 +87,21 @@ reportUnnecessaryTypeIgnoreComment = true reportMissingParameterType = true [tool.pytest.ini_options] -addopts = "-ra --verbose --color=yes" +addopts = "-rA --verbose --color=yes" [tool.coverage.run] branch = true parallel = true +omit = ["*_pb2.py"] source = ["pysparkplug"] [tool.coverage.report] show_missing = true -fail_under = 54 +fail_under = 65 +exclude_also = [ + "if TYPE_CHECKING:", + "class .*\\bProtocol\\):", +] [tool.check-wheel-contents] toplevel = "pysparkplug" diff --git a/src/pysparkplug/_client.py b/src/pysparkplug/_client.py index 7af55f1..272cd90 100644 --- a/src/pysparkplug/_client.py +++ b/src/pysparkplug/_client.py @@ -1,7 +1,7 @@ """Module containing the low-level Sparkplug B client""" import logging -from typing import Any, Callable, Dict, Optional, Tuple, Union +from typing import Any, Callable, Optional, Union from paho.mqtt import client as paho_mqtt @@ -43,8 +43,8 @@ class Client: """ _client: paho_mqtt.Client - _subscriptions: Dict[Topic, QoS] - _births: Dict[Tuple[Optional[str], Optional[str], Optional[str]], Birth] + _subscriptions: dict[Topic, QoS] + _births: dict[tuple[Optional[str], Optional[str], Optional[str]], Birth] def __init__( self, @@ -145,8 +145,8 @@ def connect( def cb( _client: paho_mqtt.Client, - _userdata: Dict[Any, Any], - _flags: Dict[Any, Any], + _userdata: dict[Any, Any], + _flags: dict[Any, Any], rc: int, ) -> None: self._on_connect(rc) @@ -219,7 +219,7 @@ def subscribe( def cb( _client: paho_mqtt.Client, - _userdata: Dict[Any, Any], + _userdata: dict[Any, Any], mqtt_message: paho_mqtt.MQTTMessage, ) -> None: message = self._handle_message(mqtt_message) diff --git a/src/pysparkplug/_config.py b/src/pysparkplug/_config.py index af2e473..793fe95 100644 --- a/src/pysparkplug/_config.py +++ b/src/pysparkplug/_config.py @@ -2,7 +2,7 @@ import dataclasses import ssl -from typing import Any, Callable, Dict, Optional, Union +from typing import Any, Callable, Optional, Union __all__ = [ "ClientOptions", @@ -89,7 +89,7 @@ class WSConfig: path: str = "/mqtt" headers: Optional[ - Union[Dict[str, Any], Callable[[Dict[str, Any]], Dict[str, Any]]] + Union[dict[str, Any], Callable[[dict[str, Any]], dict[str, Any]]] ] = None diff --git a/src/pysparkplug/_datatype.py b/src/pysparkplug/_datatype.py index 1e29b83..0bd3c3d 100644 --- a/src/pysparkplug/_datatype.py +++ b/src/pysparkplug/_datatype.py @@ -2,7 +2,8 @@ import datetime import enum -from typing import Union +import struct +from typing import Sequence, Union from pysparkplug._types import MetricValue @@ -28,8 +29,25 @@ class DataType(enum.IntEnum): DATETIME = 13 TEXT = 14 UUID = 15 + DATASET = 16 #: Unsupported BYTES = 17 FILE = 18 + TEMPLATE = 19 #: Unsupported + PROPERTYSET = 20 #: Unsupported + PROPERTYSETLIST = 21 #: Unsupported + INT8_ARRAY = 22 + INT16_ARRAY = 23 + INT32_ARRAY = 24 + INT64_ARRAY = 25 + UINT8_ARRAY = 26 + UINT16_ARRAY = 27 + UINT32_ARRAY = 28 + UINT64_ARRAY = 29 + FLOAT_ARRAY = 30 + DOUBLE_ARRAY = 31 + BOOLEAN_ARRAY = 32 + STRING_ARRAY = 33 + DATETIME_ARRAY = 34 @property def field(self) -> str: @@ -37,14 +55,14 @@ def field(self) -> str: try: return _fields[self] except KeyError as exc: - raise ValueError(f"{self} has no field name") from exc + raise NotImplementedError(f"{self.name} not implemented") from exc def encode(self, value: MetricValue) -> Union[int, float, bool, str, bytes]: """Encode a value into the form it should take in a Sparkplug B Protobuf object""" try: encoder = _encoders[self] except KeyError as exc: - raise ValueError(f"{self} cannot be encoded") from exc + raise NotImplementedError(f"{self.name} not implemented") from exc return encoder(value) def decode(self, value: Union[int, float, bool, str, bytes]) -> MetricValue: @@ -52,7 +70,7 @@ def decode(self, value: Union[int, float, bool, str, bytes]) -> MetricValue: try: decoder = _decoders[self] except KeyError as exc: - raise ValueError(f"{self} cannot be decoded") from exc + raise NotImplementedError(f"{self.name} not implemented") from exc return decoder(value) @@ -74,39 +92,160 @@ def decode(self, value: Union[int, float, bool, str, bytes]) -> MetricValue: DataType.UUID: "string_value", DataType.BYTES: "bytes_value", DataType.FILE: "bytes_value", + DataType.INT8_ARRAY: "bytes_value", + DataType.INT16_ARRAY: "bytes_value", + DataType.INT32_ARRAY: "bytes_value", + DataType.INT64_ARRAY: "bytes_value", + DataType.UINT8_ARRAY: "bytes_value", + DataType.UINT16_ARRAY: "bytes_value", + DataType.UINT32_ARRAY: "bytes_value", + DataType.UINT64_ARRAY: "bytes_value", + DataType.FLOAT_ARRAY: "bytes_value", + DataType.DOUBLE_ARRAY: "bytes_value", + DataType.BOOLEAN_ARRAY: "bytes_value", + DataType.STRING_ARRAY: "bytes_value", + DataType.DATETIME_ARRAY: "bytes_value", } + +def _uint_coder(value: int, bits: int) -> int: + if not 0 <= value < 2**bits: + raise OverflowError(f"UInt{bits} overflow with value {value}") + return value + + +def _int_encoder(value: int, bits: int) -> int: + max_val: int = 2 ** (bits - 1) + if not -max_val <= value < max_val: + raise OverflowError(f"Int{bits} overflow with value {value}") + return value + (max_val * 2 if value < 0 else 0) + + +def _int_decoder(value: int, bits: int) -> int: + max_val: int = 2**bits + if not 0 <= value < max_val: + raise OverflowError(f"Int{bits} overflow with value {value}") + return value - (max_val if value >= 2 ** (bits - 1) else 0) + + +def _encode_numeric_array(values: Sequence[float], format_char: str) -> bytes: + """Convert array to bytes using specified format + + Args: + values: list of values to encode + format_char: Format character for struct.pack: + 'b' = int8, 'B' = uint8 + 'h' = int16, 'H' = uint16 + 'i' = int32, 'I' = uint32 + 'q' = int64, 'Q' = uint64 + 'f' = float, 'd' = double + """ + format_str = f"<{len(values)}{format_char}" # '<' for little endian + try: + return struct.pack(format_str, *values) + except struct.error as exc: + raise ValueError(f"Failed to encode array with format {format_char}") from exc + + +def _decode_numeric_array(data: bytes, format_char: str) -> tuple[float, ...]: + """Convert bytes back to array using specified format + + Args: + data: Bytes to decode + format_char: Format character for struct.unpack: + 'b' = int8, 'B' = uint8 + 'h' = int16, 'H' = uint16 + 'i' = int32, 'I' = uint32 + 'q' = int64, 'Q' = uint64 + 'f' = float, 'd' = double + """ + size = struct.calcsize(format_char) + count = len(data) // size + format_str = f"<{count}{format_char}" # '<' for little endian + try: + return tuple(struct.unpack(format_str, data)) + except struct.error as exc: + raise ValueError(f"Failed to decode array with format {format_char}") from exc + + +def _encode_boolean_array(bools: tuple[bool, ...]) -> bytes: + num_bits = len(bools) + num_bytes = (num_bits + 7) // 8 + + # 4-byte integer that represents the total number of boolean values + first_byte = num_bits.to_bytes(4, "little") + + # Form bitstring, right zero pad for the number of bytes + bitstring = "".join("1" if byte else "0" for byte in bools) + "0" * ( + num_bytes * 8 - num_bits + ) + + # Convert to an integer, handling empty tuple = 0 safely + int_value = int(bitstring, 2) if bitstring else 0 + + # Prepend the first byte with the integer converted to bytes + return first_byte + int_value.to_bytes(num_bytes, byteorder="big") + + +def _decode_boolean_array(data: bytes) -> tuple[bool, ...]: + num_bits = int.from_bytes(data[:4], "little") + masks = tuple(1 << ind for ind in range(8))[::-1] + return tuple(bool(byte & mask) for byte in data[4:] for mask in masks)[:num_bits] + + _encoders = { + # Unsigned integers DataType.UINT8: lambda val: _uint_coder(val, 8), DataType.UINT16: lambda val: _uint_coder(val, 16), DataType.UINT32: lambda val: _uint_coder(val, 32), DataType.UINT64: lambda val: _uint_coder(val, 64), + # Signed integers DataType.INT8: lambda val: _int_encoder(val, 8), DataType.INT16: lambda val: _int_encoder(val, 16), DataType.INT32: lambda val: _int_encoder(val, 32), DataType.INT64: lambda val: _int_encoder(val, 64), + # Other scalar types DataType.FLOAT: lambda val: val, DataType.DOUBLE: lambda val: val, DataType.BOOLEAN: lambda val: val, DataType.STRING: lambda val: val, - DataType.DATETIME: lambda val: int( - val.replace(tzinfo=datetime.timezone.utc).timestamp() * 1e3 - ), + DataType.DATETIME: lambda val: int(val.timestamp() * 1000), DataType.TEXT: lambda val: val, DataType.UUID: lambda val: val, DataType.BYTES: lambda val: val, DataType.FILE: lambda val: val, + # Numeric arrays + DataType.INT8_ARRAY: lambda val: _encode_numeric_array(val, "b"), + DataType.INT16_ARRAY: lambda val: _encode_numeric_array(val, "h"), + DataType.INT32_ARRAY: lambda val: _encode_numeric_array(val, "i"), + DataType.INT64_ARRAY: lambda val: _encode_numeric_array(val, "q"), + DataType.UINT8_ARRAY: lambda val: _encode_numeric_array(val, "B"), + DataType.UINT16_ARRAY: lambda val: _encode_numeric_array(val, "H"), + DataType.UINT32_ARRAY: lambda val: _encode_numeric_array(val, "I"), + DataType.UINT64_ARRAY: lambda val: _encode_numeric_array(val, "Q"), + DataType.FLOAT_ARRAY: lambda val: _encode_numeric_array(val, "f"), + DataType.DOUBLE_ARRAY: lambda val: _encode_numeric_array(val, "d"), + # Other arrays + DataType.BOOLEAN_ARRAY: _encode_boolean_array, + DataType.STRING_ARRAY: lambda val: b"\0".join(s.encode("utf-8") for s in val) + + b"\0", + DataType.DATETIME_ARRAY: lambda val: _encode_numeric_array( + tuple(int(v.timestamp() * 1000) for v in val), "Q" + ), } _decoders = { + # Unsigned integers DataType.UINT8: lambda val: _uint_coder(val, 8), DataType.UINT16: lambda val: _uint_coder(val, 16), DataType.UINT32: lambda val: _uint_coder(val, 32), DataType.UINT64: lambda val: _uint_coder(val, 64), + # Signed integers DataType.INT8: lambda val: _int_decoder(val, 8), DataType.INT16: lambda val: _int_decoder(val, 16), DataType.INT32: lambda val: _int_decoder(val, 32), DataType.INT64: lambda val: _int_decoder(val, 64), + # Other scalar types DataType.FLOAT: lambda val: val, DataType.DOUBLE: lambda val: val, DataType.BOOLEAN: lambda val: val, @@ -118,24 +257,24 @@ def decode(self, value: Union[int, float, bool, str, bytes]) -> MetricValue: DataType.UUID: lambda val: val, DataType.BYTES: lambda val: val, DataType.FILE: lambda val: val, + # Numeric array + DataType.INT8_ARRAY: lambda val: _decode_numeric_array(val, "b"), + DataType.INT16_ARRAY: lambda val: _decode_numeric_array(val, "h"), + DataType.INT32_ARRAY: lambda val: _decode_numeric_array(val, "i"), + DataType.INT64_ARRAY: lambda val: _decode_numeric_array(val, "q"), + DataType.UINT8_ARRAY: lambda val: _decode_numeric_array(val, "B"), + DataType.UINT16_ARRAY: lambda val: _decode_numeric_array(val, "H"), + DataType.UINT32_ARRAY: lambda val: _decode_numeric_array(val, "I"), + DataType.UINT64_ARRAY: lambda val: _decode_numeric_array(val, "Q"), + DataType.FLOAT_ARRAY: lambda val: _decode_numeric_array(val, "f"), + DataType.DOUBLE_ARRAY: lambda val: _decode_numeric_array(val, "d"), + # Other arrays + DataType.BOOLEAN_ARRAY: _decode_boolean_array, + DataType.STRING_ARRAY: lambda val: tuple( + s.decode("utf-8") for s in val.split(b"\0")[:-1] + ), + DataType.DATETIME_ARRAY: lambda val: tuple( + datetime.datetime.fromtimestamp(v / 1000, datetime.timezone.utc) + for v in _decode_numeric_array(val, "Q") + ), } - - -def _uint_coder(value: int, bits: int) -> int: - if not 0 <= value < 2**bits: - raise OverflowError(f"UInt{bits} overflow with value {value}") - return value - - -def _int_encoder(value: int, bits: int) -> int: - max_val: int = 2 ** (bits - 1) - if not -max_val <= value < max_val: - raise OverflowError(f"Int{bits} overflow with value {value}") - return value + (max_val * 2 if value < 0 else 0) - - -def _int_decoder(value: int, bits: int) -> int: - max_val: int = 2**bits - if not 0 <= value < max_val: - raise OverflowError(f"Int{bits} overflow with value {value}") - return value - (max_val if value >= 2 ** (bits - 1) else 0) diff --git a/src/pysparkplug/_edge_node.py b/src/pysparkplug/_edge_node.py index 69586f2..4767cd8 100644 --- a/src/pysparkplug/_edge_node.py +++ b/src/pysparkplug/_edge_node.py @@ -4,7 +4,7 @@ import itertools import logging -from typing import Callable, Dict, Iterable, Optional +from typing import Callable, Iterable, Optional from pysparkplug._client import Client from pysparkplug._constants import ( @@ -54,8 +54,8 @@ class EdgeNode: group_id: str edge_node_id: str - _metrics: Dict[str, Metric] - _devices: Dict[str, Device] + _metrics: dict[str, Metric] + _devices: dict[str, Device] _client: Client _bd_seq_metric: Metric @@ -188,7 +188,7 @@ def callback(client: Client) -> None: d_birth = DBirth( timestamp=get_current_timestamp(), seq=self._seq, - metrics=device.metrics.values(), + metrics=tuple(device.metrics.values()), ) client.publish( Message( @@ -265,7 +265,7 @@ def register(self, device: Device) -> None: d_birth = DBirth( timestamp=get_current_timestamp(), seq=self._seq, - metrics=device.metrics.values(), + metrics=tuple(device.metrics.values()), ) self._client.publish( Message( @@ -355,12 +355,12 @@ def unsubscribe(self, topic: Topic) -> None: self._client.unsubscribe(topic) @property - def metrics(self) -> Dict[str, Metric]: + def metrics(self) -> dict[str, Metric]: """Returns a copy of the metrics for this edge node in a dictionary""" return self._metrics.copy() @property - def devices(self) -> Dict[str, Device]: + def devices(self) -> dict[str, Device]: """Returns a copy of the devices for this edge node in a dictionary""" return self._devices.copy() @@ -395,7 +395,7 @@ def update(self, metrics: Iterable[Metric]) -> None: edge_node_id=self.edge_node_id, ) n_data = NData( - timestamp=get_current_timestamp(), seq=self._seq, metrics=metrics + timestamp=get_current_timestamp(), seq=self._seq, metrics=tuple(metrics) ) self._client.publish( Message(topic=topic, payload=n_data, qos=QoS.AT_MOST_ONCE, retain=False), @@ -425,7 +425,7 @@ def update_device(self, device_id: str, metrics: Iterable[Metric]) -> None: edge_node_id=self.edge_node_id, device_id=device_id, ) - d_data = DData(get_current_timestamp(), seq=self._seq, metrics=metrics) + d_data = DData(get_current_timestamp(), seq=self._seq, metrics=tuple(metrics)) self._client.publish( Message( topic=d_data_topic, payload=d_data, qos=QoS.AT_MOST_ONCE, retain=False @@ -457,7 +457,7 @@ class Device: """ device_id: str - _metrics: Dict[str, Metric] + _metrics: dict[str, Metric] cmd_callback: Callable[[EdgeNode, Message], None] def __init__( @@ -484,7 +484,7 @@ def _setup_metrics(self, metrics: Iterable[Metric]) -> None: self._metrics[metric.name] = metric @property - def metrics(self) -> Dict[str, Metric]: + def metrics(self) -> dict[str, Metric]: """Returns a copy of the metrics for this edge node in a dictionary""" return self._metrics.copy() diff --git a/src/pysparkplug/_error.py b/src/pysparkplug/_error.py index e7c3834..767ef87 100644 --- a/src/pysparkplug/_error.py +++ b/src/pysparkplug/_error.py @@ -1,6 +1,6 @@ """Module containing errors and functions to handle them""" -from typing import Optional, Set +from typing import Optional from pysparkplug._enums import ConnackCode, ErrorCode @@ -12,7 +12,7 @@ class MQTTError(Exception): def check_error_code( - error_int: int, *, ignore_codes: Optional[Set[ErrorCode]] = None + error_int: int, *, ignore_codes: Optional[set[ErrorCode]] = None ) -> None: """Validate error code""" if error_int > 0: @@ -22,7 +22,7 @@ def check_error_code( def check_connack_code( - connack_int: int, *, ignore_codes: Optional[Set[ConnackCode]] = None + connack_int: int, *, ignore_codes: Optional[set[ConnackCode]] = None ) -> None: """Validate connack code""" if connack_int > 0: diff --git a/src/pysparkplug/_payload.py b/src/pysparkplug/_payload.py index 23f6d5e..7e0a5a9 100644 --- a/src/pysparkplug/_payload.py +++ b/src/pysparkplug/_payload.py @@ -5,8 +5,7 @@ import dataclasses import json from abc import abstractmethod -from collections.abc import Iterable -from typing import Dict, Optional, Protocol, cast, runtime_checkable +from typing import Optional, Protocol, cast, runtime_checkable from pysparkplug import _protobuf as protobuf from pysparkplug._datatype import DataType @@ -129,15 +128,16 @@ class Birth(_PBPayload): timestamp: int seq: int - metrics: Iterable[Metric] - _names_mapping: Dict[int, str] = dataclasses.field( + metrics: tuple[Metric, ...] + _names_mapping: dict[int, str] = dataclasses.field( init=False, default_factory=dict, repr=False ) - _dtypes_mapping: Dict[str, DataType] = dataclasses.field( + _dtypes_mapping: dict[str, DataType] = dataclasses.field( init=False, default_factory=dict, repr=False ) def __post_init__(self) -> None: + """Validates payload""" for metric in self.metrics: if metric.name is None: raise ValueError( @@ -235,7 +235,7 @@ class DBirth(Birth): class _Data(_PBPayload): timestamp: int seq: int - metrics: Iterable[Metric] + metrics: tuple[Metric, ...] class NData(_Data): @@ -267,7 +267,7 @@ class DData(_Data): @dataclasses.dataclass(frozen=True) class _Cmd(_PBPayload): timestamp: int - metrics: Iterable[Metric] + metrics: tuple[Metric, ...] class NCmd(_Cmd): diff --git a/src/pysparkplug/_strenum.py b/src/pysparkplug/_strenum.py index f7250ff..4af0ac0 100644 --- a/src/pysparkplug/_strenum.py +++ b/src/pysparkplug/_strenum.py @@ -1,7 +1,7 @@ """Module of StrEnum backport, since it isn't packaged appropriately""" from enum import Enum -from typing import Any, List, Type, TypeVar +from typing import Any, TypeVar __all__ = ["StrEnum"] @@ -13,7 +13,7 @@ class StrEnum(str, Enum): Enum where members are also (and must be) strings """ - def __new__(cls: Type[_S], *values: str) -> _S: + def __new__(cls: type[_S], *values: str) -> _S: if len(values) > 3: raise TypeError(f"too many arguments for str(): {values}") if len(values) == 1: @@ -37,7 +37,7 @@ def __new__(cls: Type[_S], *values: str) -> _S: @staticmethod def _generate_next_value_( - name: str, start: int, count: int, last_values: List[Any] + name: str, start: int, count: int, last_values: list[Any] ) -> str: """ Return the lower-cased version of the member name. diff --git a/src/pysparkplug/_types.py b/src/pysparkplug/_types.py index bd41d64..1299986 100644 --- a/src/pysparkplug/_types.py +++ b/src/pysparkplug/_types.py @@ -11,5 +11,19 @@ __all__ = ["MetricValue", "Self", "TypeAlias"] -MetricValue: TypeAlias = Union[int, float, bool, str, bytes, datetime.datetime] +MetricValue: TypeAlias = Union[ + # Scalar types + int, + float, + bool, + str, + bytes, + datetime.datetime, + # Array types + tuple[int, ...], + tuple[float, ...], + tuple[bool, ...], + tuple[str, ...], + tuple[datetime.datetime, ...], +] """Type annotation for the types a `Metric`'s `value` attribute can take""" diff --git a/test/unit_tests/test_datatype.py b/test/unit_tests/test_datatype.py new file mode 100644 index 0000000..5c854f0 --- /dev/null +++ b/test/unit_tests/test_datatype.py @@ -0,0 +1,978 @@ +"""Unit tests for DataType functionality""" + +import math +import unittest +from datetime import datetime, timedelta, timezone +from typing import TypeVar, Union, cast + +import numpy as np + +from pysparkplug import DataType, Metric, MetricValue, NCmd + +SpecificMetricValue = TypeVar("SpecificMetricValue", bound=MetricValue) + + +def encode_decode( + value: SpecificMetricValue, datatype: DataType +) -> SpecificMetricValue: + """Send the value as the specified data type through full serialization stack""" + metric = Metric(timestamp=0, name="test", datatype=datatype, value=value) + n_cmd = NCmd(timestamp=0, metrics=(metric,)) + raw = n_cmd.encode(include_dtypes=True) + other_n_cmd = NCmd.decode(raw=raw) + other_metric = other_n_cmd.metrics[0] + return cast(SpecificMetricValue, other_metric.value) + + +def check_float( + test_case: unittest.TestCase, value: float, expected: float, datatype: DataType +) -> None: + if math.isnan(expected): + test_case.assertTrue( + math.isnan(value), f"Expected NaN, got {value} for {datatype}" + ) + elif math.isinf(expected): + test_case.assertTrue( + math.isinf(value), + f"Expected {expected}, got {value} for {datatype}", + ) + # Check sign of infinity + test_case.assertEqual( + math.copysign(1, expected), + math.copysign(1, value), + f"Infinity sign mismatch for {datatype}", + ) + else: + # For normal numbers, compare with relative tolerance + test_case.assertAlmostEqual( + value, + expected, + delta=delta( + expected, single=datatype in {DataType.FLOAT, DataType.FLOAT_ARRAY} + ), + msg=f"Value mismatch for {datatype}: expected {expected}, got {value}", + ) + # Check sign of zero + if expected == 0: + test_case.assertEqual( + math.copysign(1, expected), + math.copysign(1, value), + f"Zero sign mismatch for {datatype}", + ) + + +def delta(value: Union[float, np.single], single: bool = False): + """Expected float precision error for this value""" + if single: + value = np.single(value) + return np.nextafter(value, np.inf) - value + + +class TestScalarTypes(unittest.TestCase): + """Test scalar datatype functionality""" + + def test_integer_encoding(self): + """Test integer encoding/decoding with edge cases for all integer types""" + test_cases = [ + # INT8 (-128 to 127) + (DataType.INT8, -128, False), # Min INT8 + (DataType.INT8, 127, False), # Max INT8 + (DataType.INT8, -129, True), # Below MIN - should raise + (DataType.INT8, 128, True), # Above MAX - should raise + # UINT8 (0 to 255) + (DataType.UINT8, 0, False), # Min UINT8 + (DataType.UINT8, 255, False), # Max UINT8 + (DataType.UINT8, -1, True), # Below MIN - should raise + (DataType.UINT8, 256, True), # Above MAX - should raise + # INT16 (-32768 to 32767) + (DataType.INT16, -32768, False), # Min INT16 + (DataType.INT16, 32767, False), # Max INT16 + (DataType.INT16, -32769, True), # Below MIN - should raise + (DataType.INT16, 32768, True), # Above MAX - should raise + # UINT16 (0 to 65535) + (DataType.UINT16, 0, False), # Min UINT16 + (DataType.UINT16, 65535, False), # Max UINT16 + (DataType.UINT16, -1, True), # Below MIN - should raise + (DataType.UINT16, 65536, True), # Above MAX - should raise + # INT32 (-2147483648 to 2147483647) + (DataType.INT32, -2147483648, False), # Min INT32 + (DataType.INT32, 2147483647, False), # Max INT32 + (DataType.INT32, -2147483649, True), # Below MIN - should raise + (DataType.INT32, 2147483648, True), # Above MAX - should raise + # UINT32 (0 to 4294967295) + (DataType.UINT32, 0, False), # Min UINT32 + (DataType.UINT32, 4294967295, False), # Max UINT32 + (DataType.UINT32, -1, True), # Below MIN - should raise + (DataType.UINT32, 4294967296, True), # Above MAX - should raise + # INT64 (-9223372036854775808 to 9223372036854775807) + (DataType.INT64, -9223372036854775808, False), # Min INT64 + (DataType.INT64, 9223372036854775807, False), # Max INT64 + (DataType.INT64, -9223372036854775809, True), # Below MIN - should raise + (DataType.INT64, 9223372036854775808, True), # Above MAX - should raise + # UINT64 (0 to 18446744073709551615) + (DataType.UINT64, 0, False), # Min UINT64 + (DataType.UINT64, 18446744073709551615, False), # Max UINT64 + (DataType.UINT64, -1, True), # Below MIN - should raise + (DataType.UINT64, 18446744073709551616, True), # Above MAX - should raise + # Additional edge cases + (DataType.INT8, 0, False), # Zero for signed + (DataType.UINT8, 0, False), # Zero for unsigned + (DataType.INT64, 1, False), # Small positive for large type + (DataType.UINT64, 1, False), # Small positive for large type + (DataType.INT32, -1, False), # Negative one + (DataType.INT16, -1, False), # Negative one in smaller type + ] + + for dtype, value, should_raise in test_cases: + with self.subTest(dtype=dtype, value=value): + if should_raise: + with self.assertRaises(OverflowError): + dtype.encode(value) + else: + decoded = encode_decode(value, datatype=dtype) + self.assertEqual( + decoded, value, f"Failed to encode/decode {value} with {dtype}" + ) + + def test_decoder_errors(self) -> None: + with self.assertRaises(OverflowError): + # too large + DataType.INT8.decode(2**9) + + with self.assertRaises(ValueError): + # not enough bytes + DataType.INT16_ARRAY.decode(b"\x00") + + def test_float_encoding(self): + """Test floating point encoding/decoding with edge cases + + Tests both FLOAT (32-bit) and DOUBLE (64-bit) types, including: + - Normal numbers + - Denormalized numbers + - Special values (inf, -inf, nan) + - Edge cases (max/min values) + - Zero and negative zero + """ + test_cases = [ + # Normal numbers - FLOAT + (DataType.FLOAT, 0.0), + (DataType.FLOAT, 1.0), + (DataType.FLOAT, -1.0), + (DataType.FLOAT, 3.14159), + (DataType.FLOAT, -3.14159), + # Normal numbers - DOUBLE + (DataType.DOUBLE, 0.0), + (DataType.DOUBLE, 1.0), + (DataType.DOUBLE, -1.0), + (DataType.DOUBLE, math.pi), + (DataType.DOUBLE, -math.pi), + (DataType.DOUBLE, math.e), + # Special values - FLOAT + (DataType.FLOAT, float("inf")), + (DataType.FLOAT, float("-inf")), + (DataType.FLOAT, float("nan")), + # Special values - DOUBLE + (DataType.DOUBLE, float("inf")), + (DataType.DOUBLE, float("-inf")), + (DataType.DOUBLE, float("nan")), + # Denormalized numbers - FLOAT + (DataType.FLOAT, 1.4e-45), # Smallest positive denormal float + (DataType.FLOAT, -1.4e-45), # Largest negative denormal float + # Denormalized numbers - DOUBLE + (DataType.DOUBLE, 4.9e-324), # Smallest positive denormal double + (DataType.DOUBLE, -4.9e-324), # Largest negative denormal double + # Edge cases - FLOAT + (DataType.FLOAT, 3.4e38), # Near max float + (DataType.FLOAT, -3.4e38), # Near min float + (DataType.FLOAT, 1.2e-38), # Near smallest normal float + (DataType.FLOAT, -1.2e-38), # Near largest normal negative float + # Edge cases - DOUBLE + (DataType.DOUBLE, 1.8e308), # Near max double + (DataType.DOUBLE, -1.8e308), # Near min double + (DataType.DOUBLE, 2.2e-308), # Near smallest normal double + (DataType.DOUBLE, -2.2e-308), # Near largest normal negative double + # Zero representations + (DataType.FLOAT, 0.0), + (DataType.FLOAT, -0.0), + (DataType.DOUBLE, 0.0), + (DataType.DOUBLE, -0.0), + # Specific decimal values + (DataType.FLOAT, 0.1), + (DataType.FLOAT, 0.2), + (DataType.DOUBLE, 0.1), + (DataType.DOUBLE, 0.2), + # Powers of 2 + (DataType.FLOAT, 2.0), + (DataType.FLOAT, 4.0), + (DataType.FLOAT, 8.0), + (DataType.DOUBLE, 2.0), + (DataType.DOUBLE, 4.0), + (DataType.DOUBLE, 8.0), + ] + + for dtype, value in test_cases: + with self.subTest(dtype=dtype, value=value): + decoded = cast(float, encode_decode(value, datatype=dtype)) + check_float(self, decoded, value, dtype) + + def test_datetime_encoding(self): + """Test datetime encoding/decoding with timezones""" + test_cases = [ + # UTC timezone + (datetime(2023, 1, 1, tzinfo=timezone.utc), 1672531200000), + # Non-UTC timezone + ( + datetime(2023, 1, 1, 12, 0, tzinfo=timezone(timedelta(hours=5))), + 1672556400000, # 2023-01-01 07:00:00 UTC + ), + # Timezone naive + (datetime(2023, 1, 1), None), + # Edge cases + (datetime(1970, 1, 1, tzinfo=timezone.utc), 0), # Epoch + ( + datetime(2038, 1, 19, 3, 14, 7, tzinfo=timezone.utc), + 2147483647000, + ), # 32-bit limit (2^31 - 1 seconds) + ] + + for value, expected in test_cases: + with self.subTest(datetime=value): + encoded = DataType.DATETIME.encode(value) + if expected is not None: + self.assertEqual(encoded, expected) + decoded = cast(datetime, encode_decode(value, DataType.DATETIME)) + self.assertLess(abs(decoded.timestamp() - value.timestamp()), 1e-3) + + def test_string_encoding(self) -> None: + """Test string encoding/decoding with comprehensive character sets and edge cases + + Tests: + - Empty and basic strings + - Unicode characters from different planes + - Special characters + - Length limits and boundary cases + - Mixed content strings + - Various newline combinations + """ + test_cases = [ + # Empty and basic strings + "", # Empty string + "Hello", # Basic ASCII + "Hello123", # ASCII with numbers + # Unicode characters + "Hello ไธ–็•Œ", # Basic Unicode + "ใ“ใ‚“ใซใกใฏ", # Japanese + "์•ˆ๋…•ํ•˜์„ธ์š”", # Korean + "ะŸั€ะธะฒะตั‚", # Cyrillic + "ู…ุฑุญุจุง", # Arabic + "ืฉึธืืœื•ึนื", # Hebrew with diacritics + # Emojis and symbols + "Hello ๐Ÿ‘‹ World ๐ŸŒ", # Basic emojis + "โค๏ธ ๐Ÿ’” ๐Ÿ’ ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ", # Emojis with modifiers and ZWJ sequences + "โ„ข ยฎ ยฉ ยฅ ยฃ โ‚ฌ ยข โ‚ฟ", # Special symbols + # Control characters + "Line1\nLine2", # Newline + "Column1\tColumn2", # Tab + "Text\rReturn", # Carriage return + "Mixed\r\nNewlines", # CRLF + "Bell\aBell", # Bell + "Back\bspace", # Backspace + "Form\ffeed", # Form feed + "Vertical\vtab", # Vertical tab + # Special characters + 'Quote"Quote', # Double quote + "Single'Quote", # Single quote + "Back\\slash", # Backslash + "Null\x00Char", # Null character + # Mixed content + "ASCII-123 ไธ–็•Œ ๐Ÿ‘‹ \n\t\r", # Mix of different types + "Mixed\x00With\x00Null", # Multiple null characters + "๐ŸŒ\nไธ–็•Œ\r์•ˆ๋…•", # Unicode with control chars + # Length edge cases + "x", # Single character + "x" * 100, # Medium length + "x" * 1000, # Large string + "x" * 10000, # XL string + "x" * 65535, # exactly at UINT16_MAX + "x" * 65536, # exceeding UINT16_MAX + "x" * 100000, # XXL string + "ไธ–็•Œ" * 32767, # Just under UINT16_MAX in UTF-8 bytes + ] + + for text in test_cases: + with self.subTest(text=text[:50] + "..." if len(text) > 50 else text): + decoded = cast(str, encode_decode(text, DataType.TEXT)) + self.assertEqual(decoded, text) + + def test_not_implemented(self) -> None: + with self.assertRaises(NotImplementedError): + DataType.TEMPLATE.field + + with self.assertRaises(NotImplementedError): + DataType.TEMPLATE.encode(0) + + with self.assertRaises(NotImplementedError): + DataType.TEMPLATE.decode(0) + + +class TestArrayTypes(unittest.TestCase): + """Test array type functionality""" + + def test_integer_array_validation(self): + """Test integer array validation with boundaries and overflow cases""" + test_cases = [ + # INT8 (-128 to 127) + (DataType.INT8_ARRAY, (-128,), None), # Min value + (DataType.INT8_ARRAY, (127,), None), # Max value + (DataType.INT8_ARRAY, (-128, -1, 0, 1, 127), None), # Range of values + (DataType.INT8_ARRAY, (-129,), ValueError), # Below min + (DataType.INT8_ARRAY, (128,), ValueError), # Above max + (DataType.INT8_ARRAY, (-128, 128), ValueError), # Valid and invalid + # UINT8 (0 to 255) + (DataType.UINT8_ARRAY, (0,), None), # Min value + (DataType.UINT8_ARRAY, (255,), None), # Max value + (DataType.UINT8_ARRAY, (0, 1, 128, 255), None), # Range of values + (DataType.UINT8_ARRAY, (-1,), ValueError), # Below min + (DataType.UINT8_ARRAY, (256,), ValueError), # Above max + (DataType.UINT8_ARRAY, (0, 256), ValueError), # Valid and invalid + # INT16 (-32768 to 32767) + (DataType.INT16_ARRAY, (-32768,), None), # Min value + (DataType.INT16_ARRAY, (32767,), None), # Max value + (DataType.INT16_ARRAY, (-32768, -1, 0, 1, 32767), None), # Range of values + (DataType.INT16_ARRAY, (-32769,), ValueError), # Below min + (DataType.INT16_ARRAY, (32768,), ValueError), # Above max + (DataType.INT16_ARRAY, (-32768, 32768), ValueError), # Valid and invalid + # UINT16 (0 to 65535) + (DataType.UINT16_ARRAY, (0,), None), # Min value + (DataType.UINT16_ARRAY, (65535,), None), # Max value + (DataType.UINT16_ARRAY, (0, 1, 32768, 65535), None), # Range of values + (DataType.UINT16_ARRAY, (-1,), ValueError), # Below min + (DataType.UINT16_ARRAY, (65536,), ValueError), # Above max + (DataType.UINT16_ARRAY, (0, 65536), ValueError), # Valid and invalid + # INT32 (-2147483648 to 2147483647) + (DataType.INT32_ARRAY, (-2147483648,), None), # Min value + (DataType.INT32_ARRAY, (2147483647,), None), # Max value + ( + DataType.INT32_ARRAY, + (-2147483648, -1, 0, 1, 2147483647), + None, + ), # Range of values + (DataType.INT32_ARRAY, (-2147483649,), ValueError), # Below min + (DataType.INT32_ARRAY, (2147483648,), ValueError), # Above max + ( + DataType.INT32_ARRAY, + (-2147483648, 2147483648), + ValueError, + ), # Valid and invalid + # UINT32 (0 to 4294967295) + (DataType.UINT32_ARRAY, (0,), None), # Min value + (DataType.UINT32_ARRAY, (4294967295,), None), # Max value + ( + DataType.UINT32_ARRAY, + (0, 1, 2147483648, 4294967295), + None, + ), # Range of values + (DataType.UINT32_ARRAY, (-1,), ValueError), # Below min + (DataType.UINT32_ARRAY, (4294967296,), ValueError), # Above max + ( + DataType.UINT32_ARRAY, + (0, 4294967296), + ValueError, + ), # Valid and invalid + # INT64 (-9223372036854775808 to 9223372036854775807) + (DataType.INT64_ARRAY, (-9223372036854775808,), None), # Min value + (DataType.INT64_ARRAY, (9223372036854775807,), None), # Max value + ( + DataType.INT64_ARRAY, + (-9223372036854775808, -1, 0, 1, 9223372036854775807), + None, + ), # Range + (DataType.INT64_ARRAY, (-9223372036854775809,), ValueError), # Below min + (DataType.INT64_ARRAY, (9223372036854775808,), ValueError), # Above max + ( + DataType.INT64_ARRAY, + (-9223372036854775808, 9223372036854775808), + ValueError, + ), # Valid and invalid + # UINT64 (0 to 18446744073709551615) + (DataType.UINT64_ARRAY, (0,), None), # Min value + (DataType.UINT64_ARRAY, (18446744073709551615,), None), # Max value + ( + DataType.UINT64_ARRAY, + (0, 1, 9223372036854775808, 18446744073709551615), + None, + ), # Range + (DataType.UINT64_ARRAY, (-1,), ValueError), # Below min + (DataType.UINT64_ARRAY, (18446744073709551616,), ValueError), # Above max + ( + DataType.UINT64_ARRAY, + (0, 18446744073709551616), + ValueError, + ), # Valid and invalid + ] + + for dtype, value, expected_error in test_cases: + with self.subTest(dtype=dtype, value=value): + if expected_error is None: + decoded = encode_decode(value, dtype) + self.assertEqual(decoded, value) + else: + with self.assertRaises(expected_error): + dtype.encode(value) + + def test_float_array_validation(self): + """Test floating point array validation""" + test_cases = [ + # FLOAT basic cases + (DataType.FLOAT_ARRAY, (0.0,)), # Zero + (DataType.FLOAT_ARRAY, (-1.0, 1.0)), # Unit values + (DataType.FLOAT_ARRAY, (3.14159, -3.14159)), # Common pi + # FLOAT boundary cases + (DataType.FLOAT_ARRAY, (3.4028235e38,)), # Max float32 + (DataType.FLOAT_ARRAY, (-3.4028235e38,)), # Min float32 + (DataType.FLOAT_ARRAY, (1.17549435e-38,)), # Min positive normal + (DataType.FLOAT_ARRAY, (1.4e-45,)), # Min positive subnormal + # FLOAT special values + (DataType.FLOAT_ARRAY, (float("inf"),)), + (DataType.FLOAT_ARRAY, (float("-inf"),)), + (DataType.FLOAT_ARRAY, (float("nan"),)), + (DataType.FLOAT_ARRAY, (float("inf"), float("-inf"), float("nan"))), + # FLOAT mixed values + (DataType.FLOAT_ARRAY, (1, 1.0, 1.5)), # Mixed integers and floats + (DataType.FLOAT_ARRAY, (-3.4028235e38, 0.0, 3.4028235e38)), # Range + # DOUBLE basic cases + (DataType.DOUBLE_ARRAY, (0.0,)), # Zero + (DataType.DOUBLE_ARRAY, (-1.0, 1.0)), # Unit values + (DataType.DOUBLE_ARRAY, (math.pi, -math.pi)), # Pi + # DOUBLE boundary cases + (DataType.DOUBLE_ARRAY, (1.7976931348623157e308,)), # Max double + (DataType.DOUBLE_ARRAY, (-1.7976931348623157e308,)), # Min double + ( + DataType.DOUBLE_ARRAY, + (2.2250738585072014e-308,), + ), # Min positive normal + ( + DataType.DOUBLE_ARRAY, + (4.9406564584124654e-324,), + ), # Min positive subnormal + # DOUBLE special values + (DataType.DOUBLE_ARRAY, (float("inf"),)), + (DataType.DOUBLE_ARRAY, (float("-inf"),)), + (DataType.DOUBLE_ARRAY, (float("nan"),)), + (DataType.DOUBLE_ARRAY, (float("inf"), float("-inf"), float("nan"))), + # DOUBLE mixed values + (DataType.DOUBLE_ARRAY, (1, 1.0, 1.5)), # Mixed integers and floats + ( + DataType.DOUBLE_ARRAY, + (-1.7976931348623157e308, 0.0, 1.7976931348623157e308), + ), # Range + # Testing precision + (DataType.FLOAT_ARRAY, (1.23456789,)), # Beyond float32 precision + ( + DataType.DOUBLE_ARRAY, + (1.234567890123456789,), + ), # Beyond float64 precision + ] + + for dtype, value in test_cases: + with self.subTest(dtype=dtype, value=value): + decoded = encode_decode(value, dtype) + self.assertEqual(len(decoded), len(value)) + for orig, dec in zip(value, decoded): + check_float(self, dec, orig, dtype) + + def test_integer_array_type_errors(self): + """Test integer array validation with invalid type combinations""" + test_cases = [ + # INT8 type mixing - within array + (DataType.INT8_ARRAY, (-128, "string"), ValueError, "int8 with string"), + (DataType.INT8_ARRAY, (127, 3.14), ValueError, "int8 with float"), + (DataType.INT8_ARRAY, (0, None), ValueError, "int8 with None"), + (DataType.INT8_ARRAY, (0, [1]), ValueError, "int8 with list"), + (DataType.INT8_ARRAY, (0, {}), ValueError, "int8 with dict"), + # UINT8 type mixing - within array + (DataType.UINT8_ARRAY, (255, "abc"), ValueError, "uint8 with string"), + (DataType.UINT8_ARRAY, (0, 1.5), ValueError, "uint8 with float"), + (DataType.UINT8_ARRAY, (128, None), ValueError, "uint8 with None"), + (DataType.UINT8_ARRAY, (0, (1,)), ValueError, "uint8 with tuple"), + # INT16 type mixing - within array + (DataType.INT16_ARRAY, (-32768, "xyz"), ValueError, "int16 with string"), + (DataType.INT16_ARRAY, (32767, 2.718), ValueError, "int16 with float"), + (DataType.INT16_ARRAY, (0, {}), ValueError, "int16 with dict"), + (DataType.INT16_ARRAY, (0, set((1,))), ValueError, "int16 with set"), + # UINT16 type mixing - within array + (DataType.UINT16_ARRAY, (65535, "test"), ValueError, "uint16 with string"), + (DataType.UINT16_ARRAY, (0, 3.14159), ValueError, "uint16 with float"), + (DataType.UINT16_ARRAY, (1, []), ValueError, "uint16 with empty list"), + # INT32 type mixing - within array + ( + DataType.INT32_ARRAY, + (-2147483648, "large"), + ValueError, + "int32 with string", + ), + ( + DataType.INT32_ARRAY, + (2147483647, 1.23e5), + ValueError, + "int32 with scientific notation", + ), + (DataType.INT32_ARRAY, (0, (1,)), ValueError, "int32 with tuple"), + # UINT32 type mixing - within array + ( + DataType.UINT32_ARRAY, + (4294967295, "max"), + ValueError, + "uint32 with string", + ), + (DataType.UINT32_ARRAY, (0, 1.0), ValueError, "uint32 with float"), + (DataType.UINT32_ARRAY, (1, set()), ValueError, "uint32 with empty set"), + # INT64 type mixing - within array + ( + DataType.INT64_ARRAY, + (-9223372036854775808, "min"), + ValueError, + "int64 with string", + ), + ( + DataType.INT64_ARRAY, + (9223372036854775807, 2.0), + ValueError, + "int64 with float", + ), + (DataType.INT64_ARRAY, (0, b"bytes"), ValueError, "int64 with bytes"), + # UINT64 type mixing - within array + ( + DataType.UINT64_ARRAY, + (18446744073709551615, "max"), + ValueError, + "uint64 with string", + ), + (DataType.UINT64_ARRAY, (0, 0.0), ValueError, "uint64 with float zero"), + ( + DataType.UINT64_ARRAY, + (1, bytearray()), + ValueError, + "uint64 with bytearray", + ), + # Multiple type mixing - within array + ( + DataType.INT8_ARRAY, + (1, "str", 3.14, None), + ValueError, + "multiple mixed types", + ), + ( + DataType.UINT16_ARRAY, + (1, "str", 3.14, []), + ValueError, + "multiple mixed types", + ), + ( + DataType.INT32_ARRAY, + (1, "str", None, {}), + ValueError, + "multiple mixed types", + ), + ] + + for dtype, value, expected_error, description in test_cases: + with self.subTest(dtype=dtype, case=description): + with self.assertRaises( + expected_error, + msg=f"Failed to raise {expected_error} for {description}", + ): + dtype.encode(value) + + def test_non_iterable_inputs(self): + """Test inputs that cannot be iterated over""" + test_cases = [ + (DataType.INT8_ARRAY, None, TypeError, "None value"), + (DataType.INT16_ARRAY, 42, TypeError, "single integer"), + (DataType.UINT32_ARRAY, 3.14, TypeError, "single float"), + (DataType.UINT64_ARRAY, True, TypeError, "single boolean"), + ] + + for dtype, value, expected_error, description in test_cases: + with self.subTest(dtype=dtype, case=description): + with self.assertRaises( + expected_error, + msg=f"Failed to raise {expected_error} for {description}", + ): + dtype.encode(value) + + def test_invalid_iterable_inputs(self): + """Test inputs that are iterable but have invalid types or type combinations""" + test_cases = [ + # Invalid type combinations + (DataType.INT8_ARRAY, (1, "a", 3), ValueError, "mixed types"), + (DataType.UINT8_ARRAY, (1, None, 3), ValueError, "None"), + (DataType.INT16_ARRAY, (1, 3.14, 2), ValueError, "float"), + (DataType.UINT16_ARRAY, (1, [2], 3), ValueError, "nested list"), + (DataType.UINT32_ARRAY, (1, b"bytes", 3), ValueError, "bytes"), + # Invalid string elements + (DataType.INT8_ARRAY, ("1", "2", "3"), ValueError, "string digits"), + (DataType.UINT8_ARRAY, ("a", "b", "c"), ValueError, "strings"), + # Invalid container elements + (DataType.INT16_ARRAY, ({1}, {2}, {3}), ValueError, "sets"), + (DataType.UINT16_ARRAY, ((1,), (2,), (3,)), ValueError, "tuples"), + (DataType.INT32_ARRAY, ({"a": 1}, {"b": 2}), ValueError, "dicts"), + # Complex nested structures + (DataType.INT8_ARRAY, ((1, 2), (3, 4)), ValueError, "nested structure"), + ( + DataType.UINT8_ARRAY, + (set((1, 2)), set((3, 4))), + ValueError, + "number sets", + ), + ( + DataType.INT16_ARRAY, + ({"x": 1}, {"y": 2}), + ValueError, + "number dicts", + ), + # Non-numeric types + (DataType.INT8_ARRAY, (object(), object()), ValueError, "objects"), + ( + DataType.UINT16_ARRAY, + (lambda x: x, lambda x: x), + ValueError, + "functions", + ), + ( + DataType.INT32_ARRAY, + (complex(1, 2), complex(3, 4)), + ValueError, + "complex numbers", + ), + ] + + for dtype, value, expected_error, description in test_cases: + with self.subTest(dtype=dtype, case=description): + with self.assertRaises( + expected_error, + msg=f"Failed to raise {expected_error} for {description}", + ): + dtype.encode(value) + + def test_string_array_basic(self): + """Test string array encoding/decoding for basic cases""" + test_cases = [ + # Basic cases + ("Hello", "World"), # Simple ASCII + ("Hello", "ไธ–็•Œ", "๐Ÿ‘‹"), # Mixed Unicode and emoji + # Mixed lengths + ("", "short", "medium" * 10, "long" * 100), + # Edge cases for individual strings + ("x" * 65535,), # Max length string + # Quotes and slashes (safe special chars) + ('Quote"Quote', "Single'Quote"), # Quotes + ("Back\\slash", "Path/slash"), # Slashes + # Unicode variations + ("ๆฑ‰ๅญ—", "ั€ัƒััะบะธะน", "ุงู„ุนุฑุจูŠุฉ", "ืขึดื‘ึฐืจึดื™ืช"), + ("๐ŸŒŸ", "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ", "๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป"), + ] + + for strings in test_cases: + with self.subTest(strings=strings): + decoded = encode_decode(strings, DataType.STRING_ARRAY) + self.assertEqual(len(decoded), len(strings)) + self.assertEqual(decoded, strings) + + def test_string_array_empty_strings(self): + """Test string array encoding/decoding with empty strings""" + test_cases = [ + (("",), "single empty string"), + (("", "nonempty"), "empty and nonempty"), + (("nonempty", ""), "nonempty and empty"), + (("", "middle", ""), "empty at boundaries"), + (("nonempty", "", "nonempty"), "empty in middle"), + ] + + for strings, description in test_cases: + with self.subTest(case=description): + decoded = encode_decode(strings, DataType.STRING_ARRAY) + self.assertEqual( + len(decoded), len(strings), f"Length mismatch for {description}" + ) + self.assertEqual( + decoded, strings, f"Content mismatch for {description}" + ) + + def test_string_array_line_endings(self): + """Test string array encoding/decoding with different line endings""" + test_cases = [ + # Single line endings + (("Line1\nLine2", "Line3\nLine4"), "LF only"), + (("Line1\rLine2", "Line3\rLine4"), "CR only"), + # Mixed line endings + (("Line1\r\nLine2",), "CRLF"), + (("Line1\nLine2", "Line3\rLine4"), "Mixed LF and CR"), + # Multiple line endings + (("Line1\n\nLine2",), "Multiple LF"), + (("Line1\r\rLine2",), "Multiple CR"), + # Line endings at start/end + (("\nLine1", "Line2\n"), "LF at boundaries"), + (("\rLine1", "Line2\r"), "CR at boundaries"), + ] + + for strings, description in test_cases: + with self.subTest(case=description): + decoded = encode_decode(strings, DataType.STRING_ARRAY) + self.assertEqual(len(decoded), len(strings)) + self.assertEqual(decoded, strings) + + def test_string_array_special_chars(self): + """Test string array encoding/decoding with special characters""" + test_cases = [ + # Control characters + (("Tab\tText", "Space Text"), "tabs and spaces"), + (("\b\f\v",), "control characters"), + # Extended ASCII + (tuple(chr(i) for i in range(128, 256)), "extended ASCII"), + # Whitespace combinations + ((" ", " ", "\t", "\n", "\r", "\r\n"), "whitespace variants"), + # Mixed special characters + (("Mixed\tWith\nMany\rSpecial\fChars",), "mixed special chars"), + # Zero-width characters + (("a\u200bb", "a\u200cb"), "zero-width chars"), + # Combining characters + (("e\u0301", "n\u0303"), "combining diacritics"), + # Direction control characters + (("\u202e\u202dreverse",), "direction controls"), + ] + + for strings, description in test_cases: + with self.subTest(case=description): + decoded = encode_decode(strings, DataType.STRING_ARRAY) + self.assertEqual(len(decoded), len(strings)) + self.assertEqual(decoded, strings) + + def test_boolean_array(self) -> None: + """Test boolean array encoding/decoding""" + test_cases = ( + ((), "Empty"), + ((True,), "Single true"), + ((False,), "Single false"), + ((True, False, True, True), "Short array"), + ( + ( + True, + False, + ) + * 2**8, + "Long array", + ), + ) + for array, description in test_cases: + with self.subTest(case=description): + encoded = DataType.BOOLEAN_ARRAY.encode(array) + print(encoded) + decoded = encode_decode(array, DataType.BOOLEAN_ARRAY) + + self.assertEqual(len(decoded), len(array)) + self.assertEqual(decoded, array) + + def test_datetime_array(self): + """Test datetime array encoding/decoding""" + base_dt = datetime.now(timezone.utc) + test_cases = [ + # Basic cases + (), # Empty array + (base_dt,), # Single datetime + # Historical dates + tuple(base_dt - timedelta(days=i) for i in range(10)), + # Future dates + tuple(base_dt + timedelta(days=i) for i in range(10)), + # Mix of past and future + tuple(base_dt + timedelta(days=i) for i in range(-5, 6)), + # Edge cases + ( + datetime(1970, 1, 1, tzinfo=timezone.utc), # Epoch + datetime(2038, 1, 19, 3, 14, 7, tzinfo=timezone.utc), # 32-bit limit + datetime(2024, 1, 1, 12, 0, tzinfo=timezone.utc), # Noon UTC + datetime.now(timezone(timedelta(hours=5))).astimezone( + timezone.utc + ), # Different timezone + ), + # Specific times + ( + datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc), # Midnight + datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc), # Noon + datetime(2024, 1, 1, 23, 59, 59, tzinfo=timezone.utc), # End of day + ), + # Timezone naive datetime objects + ( + datetime(2024, 1, 1, 0, 0, 0), # Midnight + datetime(2024, 1, 1, 12, 0, 0), # Noon + datetime(2024, 1, 1, 23, 59, 59), # End of day + ), + # Non-UTC timezones + ( + datetime( + 2024, 1, 1, 0, 0, 0, tzinfo=timezone(timedelta(hours=5)) + ), # Midnight + datetime( + 2024, 1, 1, 12, 0, 0, tzinfo=timezone(timedelta(hours=5)) + ), # Noon + datetime( + 2024, 1, 1, 23, 59, 59, tzinfo=timezone(timedelta(hours=5)) + ), # End of day + ), + ] + + for datetimes in test_cases: + with self.subTest(datetimes=datetimes): + decoded = encode_decode(datetimes, DataType.DATETIME_ARRAY) + self.assertEqual(len(decoded), len(datetimes)) + for orig, dec in zip(datetimes, decoded): + self.assertLess(abs(dec.timestamp() - orig.timestamp()), 1e-3) + + def test_specification_examples(self) -> None: + test_cases = ( + (DataType.INT8_ARRAY, (-23, 123), bytes((0xE9, 0x7B))), + (DataType.INT16_ARRAY, (-30_000, 30_000), bytes((0xD0, 0x8A, 0x30, 0x75))), + ( + DataType.INT32_ARRAY, + (-1, 315338746), + bytes((0xFF, 0xFF, 0xFF, 0xFF, 0xFA, 0xAF, 0xCB, 0x12)), + ), + ( + DataType.INT64_ARRAY, + (-4270929666821191986, -3601064768563266876), + bytes( + ( + 0xCE, + 0x06, + 0x72, + 0xAC, + 0x18, + 0x9C, + 0xBA, + 0xC4, + 0xC4, + 0xBA, + 0x9C, + 0x18, + 0xAC, + 0x72, + 0x06, + 0xCE, + ) + ), + ), + (DataType.UINT8_ARRAY, (23, 250), bytes((0x17, 0xFA))), + (DataType.UINT16_ARRAY, (30, 52360), bytes((0x1E, 0x00, 0x88, 0xCC))), + ( + DataType.UINT32_ARRAY, + (52, 3293969225), + bytes((0x34, 0x00, 0x00, 0x00, 0x49, 0xFB, 0x55, 0xC4)), + ), + ( + DataType.UINT64_ARRAY, + (52, 16444743074749521625), + bytes( + ( + 0x34, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xD9, + 0x9E, + 0x02, + 0xD1, + 0xB2, + 0x76, + 0x37, + 0xE4, + ) + ), + ), + ( + DataType.FLOAT_ARRAY, + (1.23, 89.341), + bytes((0xA4, 0x70, 0x9D, 0x3F, 0x98, 0xAE, 0xB2, 0x42)), + ), + ( + DataType.DOUBLE_ARRAY, + (12.354213, 1022.9123213), + bytes( + ( + 0xD7, + 0xA2, + 0x05, + 0x68, + 0x5B, + 0xB5, + 0x28, + 0x40, + 0x8E, + 0x17, + 0x1C, + 0x6F, + 0x4C, + 0xF7, + 0x8F, + 0x40, + ) + ), + ), + ( + DataType.BOOLEAN_ARRAY, + ( + False, + False, + True, + True, + False, + True, + False, + False, + True, + True, + False, + True, + ), + bytes((0x0C, 0x00, 0x00, 0x00, 0x34, 0xD0)), + ), + ( + DataType.STRING_ARRAY, + ("ABC", "hello"), + bytes((0x41, 0x42, 0x43, 0x00, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x00)), + ), + ( + DataType.DATETIME_ARRAY, + ( + datetime(2009, 10, 21, 5, 27, 55, 335_000, tzinfo=timezone.utc), + datetime(2022, 6, 24, 21, 57, 55, tzinfo=timezone.utc), + ), + bytes( + ( + 0xC7, + 0xD0, + 0x90, + 0x75, + 0x24, + 0x01, + 0x00, + 0x00, + 0xB8, + 0xBA, + 0xB8, + 0x97, + 0x81, + 0x01, + 0x00, + 0x00, + ) + ), + ), + ) + for datatype, value, expected_bytes in test_cases: + with self.subTest(datatype=datatype): + actual_bytes = datatype.encode(value) + self.assertEqual(expected_bytes, actual_bytes) diff --git a/test/unit_tests/test_metric.py b/test/unit_tests/test_metric.py new file mode 100644 index 0000000..4814c5a --- /dev/null +++ b/test/unit_tests/test_metric.py @@ -0,0 +1,103 @@ +"""Unit tests for Metric class""" + +import unittest +from datetime import datetime, timezone + +from pysparkplug import DataType, Metric + + +class TestMetric(unittest.TestCase): + """Test Metric functionality""" + + def test_basic_metric(self): + """Test basic metric creation and conversion""" + metric = Metric( + timestamp=1234567890, + name="test_metric", + datatype=DataType.INT32, + value=42, + ) + + # Convert to protobuf + pb = metric.to_pb(include_dtype=True) + self.assertEqual(pb.timestamp, 1234567890) + self.assertEqual(pb.name, "test_metric") + self.assertEqual(pb.int_value, 42) + self.assertFalse(pb.is_null) + + # Convert back + metric2 = Metric.from_pb(pb) + self.assertEqual(metric2.timestamp, 1234567890) + self.assertEqual(metric2.name, "test_metric") + self.assertEqual(metric2.value, 42) + self.assertFalse(metric2.is_null) + + def test_array_null_handling(self): + """Test array handling with is_null flag""" + test_cases = [ + # Empty array should be treated as null + (None, True), + # Non-empty array should not be null + (("test",), False), + # Array with empty string is not null + (("",), False), + # Multiple empty strings are not null + (("", ""), False), + # Mixed content is not null + (("", "test", ""), False), + ] + + for value, should_be_null in test_cases: + with self.subTest(value=value, should_be_null=should_be_null): + metric = Metric( + timestamp=1234567890, + name="test_array", + datatype=DataType.STRING_ARRAY, + value=value, + ) + + pb = metric.to_pb(include_dtype=True) + self.assertEqual(pb.is_null, should_be_null) + + metric2 = Metric.from_pb(pb) + self.assertEqual(metric2.is_null, should_be_null) + if should_be_null: + self.assertIsNone(metric2.value) + else: + self.assertEqual(metric2.value, value) + + def test_metric_properties(self): + """Test metric property handling""" + metric = Metric( + timestamp=1234567890, + name="test_metric", + datatype=DataType.INT32, + value=42, + alias=123, + is_historical=True, + is_transient=True, + ) + + pb = metric.to_pb(include_dtype=True) + self.assertEqual(pb.alias, 123) + self.assertTrue(pb.is_historical) + self.assertTrue(pb.is_transient) + + metric2 = Metric.from_pb(pb) + self.assertEqual(metric2.alias, 123) + self.assertTrue(metric2.is_historical) + self.assertTrue(metric2.is_transient) + + def test_datetime_handling(self): + """Test datetime value handling""" + now = datetime.now(timezone.utc) + metric = Metric( + timestamp=int(now.timestamp()), # Convert to milliseconds + name="test_datetime", + datatype=DataType.DATETIME, + value=now, + ) + + pb = metric.to_pb(include_dtype=True) + metric2 = Metric.from_pb(pb) + self.assertEqual(metric2.timestamp, metric.timestamp) diff --git a/uv.lock b/uv.lock index f4a63eb..2b10650 100644 --- a/uv.lock +++ b/uv.lock @@ -723,6 +723,126 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, ] +[[package]] +name = "numpy" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78", size = 18902015 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/91/3495b3237510f79f5d81f2508f9f13fea78ebfdf07538fc7444badda173d/numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece", size = 21165245 }, + { url = "https://files.pythonhosted.org/packages/05/33/26178c7d437a87082d11019292dce6d3fe6f0e9026b7b2309cbf3e489b1d/numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04", size = 13738540 }, + { url = "https://files.pythonhosted.org/packages/ec/31/cc46e13bf07644efc7a4bf68df2df5fb2a1a88d0cd0da9ddc84dc0033e51/numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66", size = 5300623 }, + { url = "https://files.pythonhosted.org/packages/6e/16/7bfcebf27bb4f9d7ec67332ffebee4d1bf085c84246552d52dbb548600e7/numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b", size = 6901774 }, + { url = "https://files.pythonhosted.org/packages/f9/a3/561c531c0e8bf082c5bef509d00d56f82e0ea7e1e3e3a7fc8fa78742a6e5/numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd", size = 13907081 }, + { url = "https://files.pythonhosted.org/packages/fa/66/f7177ab331876200ac7563a580140643d1179c8b4b6a6b0fc9838de2a9b8/numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318", size = 19523451 }, + { url = "https://files.pythonhosted.org/packages/25/7f/0b209498009ad6453e4efc2c65bcdf0ae08a182b2b7877d7ab38a92dc542/numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8", size = 19927572 }, + { url = "https://files.pythonhosted.org/packages/3e/df/2619393b1e1b565cd2d4c4403bdd979621e2c4dea1f8532754b2598ed63b/numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326", size = 14400722 }, + { url = "https://files.pythonhosted.org/packages/22/ad/77e921b9f256d5da36424ffb711ae79ca3f451ff8489eeca544d0701d74a/numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97", size = 6472170 }, + { url = "https://files.pythonhosted.org/packages/10/05/3442317535028bc29cf0c0dd4c191a4481e8376e9f0db6bcf29703cadae6/numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131", size = 15905558 }, + { url = "https://files.pythonhosted.org/packages/8b/cf/034500fb83041aa0286e0fb16e7c76e5c8b67c0711bb6e9e9737a717d5fe/numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448", size = 21169137 }, + { url = "https://files.pythonhosted.org/packages/4a/d9/32de45561811a4b87fbdee23b5797394e3d1504b4a7cf40c10199848893e/numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195", size = 13703552 }, + { url = "https://files.pythonhosted.org/packages/c1/ca/2f384720020c7b244d22508cb7ab23d95f179fcfff33c31a6eeba8d6c512/numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57", size = 5298957 }, + { url = "https://files.pythonhosted.org/packages/0e/78/a3e4f9fb6aa4e6fdca0c5428e8ba039408514388cf62d89651aade838269/numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a", size = 6905573 }, + { url = "https://files.pythonhosted.org/packages/a0/72/cfc3a1beb2caf4efc9d0b38a15fe34025230da27e1c08cc2eb9bfb1c7231/numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669", size = 13914330 }, + { url = "https://files.pythonhosted.org/packages/ba/a8/c17acf65a931ce551fee11b72e8de63bf7e8a6f0e21add4c937c83563538/numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951", size = 19534895 }, + { url = "https://files.pythonhosted.org/packages/ba/86/8767f3d54f6ae0165749f84648da9dcc8cd78ab65d415494962c86fac80f/numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9", size = 19937253 }, + { url = "https://files.pythonhosted.org/packages/df/87/f76450e6e1c14e5bb1eae6836478b1028e096fd02e85c1c37674606ab752/numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15", size = 14414074 }, + { url = "https://files.pythonhosted.org/packages/5c/ca/0f0f328e1e59f73754f06e1adfb909de43726d4f24c6a3f8805f34f2b0fa/numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4", size = 6470640 }, + { url = "https://files.pythonhosted.org/packages/eb/57/3a3f14d3a759dcf9bf6e9eda905794726b758819df4663f217d658a58695/numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc", size = 15910230 }, + { url = "https://files.pythonhosted.org/packages/45/40/2e117be60ec50d98fa08c2f8c48e09b3edea93cfcabd5a9ff6925d54b1c2/numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b", size = 20895803 }, + { url = "https://files.pythonhosted.org/packages/46/92/1b8b8dee833f53cef3e0a3f69b2374467789e0bb7399689582314df02651/numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e", size = 13471835 }, + { url = "https://files.pythonhosted.org/packages/7f/19/e2793bde475f1edaea6945be141aef6c8b4c669b90c90a300a8954d08f0a/numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c", size = 5038499 }, + { url = "https://files.pythonhosted.org/packages/e3/ff/ddf6dac2ff0dd50a7327bcdba45cb0264d0e96bb44d33324853f781a8f3c/numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c", size = 6633497 }, + { url = "https://files.pythonhosted.org/packages/72/21/67f36eac8e2d2cd652a2e69595a54128297cdcb1ff3931cfc87838874bd4/numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692", size = 13621158 }, + { url = "https://files.pythonhosted.org/packages/39/68/e9f1126d757653496dbc096cb429014347a36b228f5a991dae2c6b6cfd40/numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a", size = 19236173 }, + { url = "https://files.pythonhosted.org/packages/d1/e9/1f5333281e4ebf483ba1c888b1d61ba7e78d7e910fdd8e6499667041cc35/numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c", size = 19634174 }, + { url = "https://files.pythonhosted.org/packages/71/af/a469674070c8d8408384e3012e064299f7a2de540738a8e414dcfd639996/numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded", size = 14099701 }, + { url = "https://files.pythonhosted.org/packages/d0/3d/08ea9f239d0e0e939b6ca52ad403c84a2bce1bde301a8eb4888c1c1543f1/numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5", size = 6174313 }, + { url = "https://files.pythonhosted.org/packages/b2/b5/4ac39baebf1fdb2e72585c8352c56d063b6126be9fc95bd2bb5ef5770c20/numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a", size = 15606179 }, + { url = "https://files.pythonhosted.org/packages/43/c1/41c8f6df3162b0c6ffd4437d729115704bd43363de0090c7f913cfbc2d89/numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c", size = 21169942 }, + { url = "https://files.pythonhosted.org/packages/39/bc/fd298f308dcd232b56a4031fd6ddf11c43f9917fbc937e53762f7b5a3bb1/numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd", size = 13711512 }, + { url = "https://files.pythonhosted.org/packages/96/ff/06d1aa3eeb1c614eda245c1ba4fb88c483bee6520d361641331872ac4b82/numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b", size = 5306976 }, + { url = "https://files.pythonhosted.org/packages/2d/98/121996dcfb10a6087a05e54453e28e58694a7db62c5a5a29cee14c6e047b/numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729", size = 6906494 }, + { url = "https://files.pythonhosted.org/packages/15/31/9dffc70da6b9bbf7968f6551967fc21156207366272c2a40b4ed6008dc9b/numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1", size = 13912596 }, + { url = "https://files.pythonhosted.org/packages/b9/14/78635daab4b07c0930c919d451b8bf8c164774e6a3413aed04a6d95758ce/numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd", size = 19526099 }, + { url = "https://files.pythonhosted.org/packages/26/4c/0eeca4614003077f68bfe7aac8b7496f04221865b3a5e7cb230c9d055afd/numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d", size = 19932823 }, + { url = "https://files.pythonhosted.org/packages/f1/46/ea25b98b13dccaebddf1a803f8c748680d972e00507cd9bc6dcdb5aa2ac1/numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d", size = 14404424 }, + { url = "https://files.pythonhosted.org/packages/c8/a6/177dd88d95ecf07e722d21008b1b40e681a929eb9e329684d449c36586b2/numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa", size = 6476809 }, + { url = "https://files.pythonhosted.org/packages/ea/2b/7fc9f4e7ae5b507c1a3a21f0f15ed03e794c1242ea8a242ac158beb56034/numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73", size = 15911314 }, + { url = "https://files.pythonhosted.org/packages/8f/3b/df5a870ac6a3be3a86856ce195ef42eec7ae50d2a202be1f5a4b3b340e14/numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8", size = 21025288 }, + { url = "https://files.pythonhosted.org/packages/2c/97/51af92f18d6f6f2d9ad8b482a99fb74e142d71372da5d834b3a2747a446e/numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4", size = 6762793 }, + { url = "https://files.pythonhosted.org/packages/12/46/de1fbd0c1b5ccaa7f9a005b66761533e2f6a3e560096682683a223631fe9/numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c", size = 19334885 }, + { url = "https://files.pythonhosted.org/packages/cc/dc/d330a6faefd92b446ec0f0dfea4c3207bb1fef3c4771d19cf4543efd2c78/numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385", size = 15828784 }, +] + +[[package]] +name = "numpy" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/fdbf6a7871703df6160b5cf3dd774074b086d278172285c52c2758b76305/numpy-2.2.1.tar.gz", hash = "sha256:45681fd7128c8ad1c379f0ca0776a8b0c6583d2f69889ddac01559dfe4390918", size = 20227662 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/c4/5588367dc9f91e1a813beb77de46ea8cab13f778e1b3a0e661ab031aba44/numpy-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5edb4e4caf751c1518e6a26a83501fda79bff41cc59dac48d70e6d65d4ec4440", size = 21213214 }, + { url = "https://files.pythonhosted.org/packages/d8/8b/32dd9f08419023a4cf856c5ad0b4eba9b830da85eafdef841a104c4fc05a/numpy-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa3017c40d513ccac9621a2364f939d39e550c542eb2a894b4c8da92b38896ab", size = 14352248 }, + { url = "https://files.pythonhosted.org/packages/84/2d/0e895d02940ba6e12389f0ab5cac5afcf8dc2dc0ade4e8cad33288a721bd/numpy-2.2.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:61048b4a49b1c93fe13426e04e04fdf5a03f456616f6e98c7576144677598675", size = 5391007 }, + { url = "https://files.pythonhosted.org/packages/11/b9/7f1e64a0d46d9c2af6d17966f641fb12d5b8ea3003f31b2308f3e3b9a6aa/numpy-2.2.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:7671dc19c7019103ca44e8d94917eba8534c76133523ca8406822efdd19c9308", size = 6926174 }, + { url = "https://files.pythonhosted.org/packages/2e/8c/043fa4418bc9364e364ab7aba8ff6ef5f6b9171ade22de8fbcf0e2fa4165/numpy-2.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4250888bcb96617e00bfa28ac24850a83c9f3a16db471eca2ee1f1714df0f957", size = 14330914 }, + { url = "https://files.pythonhosted.org/packages/f7/b6/d8110985501ca8912dfc1c3bbef99d66e62d487f72e46b2337494df77364/numpy-2.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7746f235c47abc72b102d3bce9977714c2444bdfaea7888d241b4c4bb6a78bf", size = 16379607 }, + { url = "https://files.pythonhosted.org/packages/e2/57/bdca9fb8bdaa810c3a4ff2eb3231379b77f618a7c0d24be9f7070db50775/numpy-2.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:059e6a747ae84fce488c3ee397cee7e5f905fd1bda5fb18c66bc41807ff119b2", size = 15541760 }, + { url = "https://files.pythonhosted.org/packages/97/55/3b9147b3cbc3b6b1abc2a411dec5337a46c873deca0dd0bf5bef9d0579cc/numpy-2.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f62aa6ee4eb43b024b0e5a01cf65a0bb078ef8c395e8713c6e8a12a697144528", size = 18168476 }, + { url = "https://files.pythonhosted.org/packages/00/e7/7c2cde16c9b87a8e14fdd262ca7849c4681cf48c8a774505f7e6f5e3b643/numpy-2.2.1-cp310-cp310-win32.whl", hash = "sha256:48fd472630715e1c1c89bf1feab55c29098cb403cc184b4859f9c86d4fcb6a95", size = 6570985 }, + { url = "https://files.pythonhosted.org/packages/a1/a8/554b0e99fc4ac11ec481254781a10da180d0559c2ebf2c324232317349ee/numpy-2.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:b541032178a718c165a49638d28272b771053f628382d5e9d1c93df23ff58dbf", size = 12913384 }, + { url = "https://files.pythonhosted.org/packages/59/14/645887347124e101d983e1daf95b48dc3e136bf8525cb4257bf9eab1b768/numpy-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:40f9e544c1c56ba8f1cf7686a8c9b5bb249e665d40d626a23899ba6d5d9e1484", size = 21217379 }, + { url = "https://files.pythonhosted.org/packages/9f/fd/2279000cf29f58ccfd3778cbf4670dfe3f7ce772df5e198c5abe9e88b7d7/numpy-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9b57eaa3b0cd8db52049ed0330747b0364e899e8a606a624813452b8203d5f7", size = 14388520 }, + { url = "https://files.pythonhosted.org/packages/58/b0/034eb5d5ba12d66ab658ff3455a31f20add0b78df8203c6a7451bd1bee21/numpy-2.2.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bc8a37ad5b22c08e2dbd27df2b3ef7e5c0864235805b1e718a235bcb200cf1cb", size = 5389286 }, + { url = "https://files.pythonhosted.org/packages/5d/69/6f3cccde92e82e7835fdb475c2bf439761cbf8a1daa7c07338e1e132dfec/numpy-2.2.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:9036d6365d13b6cbe8f27a0eaf73ddcc070cae584e5ff94bb45e3e9d729feab5", size = 6930345 }, + { url = "https://files.pythonhosted.org/packages/d1/72/1cd38e91ab563e67f584293fcc6aca855c9ae46dba42e6b5ff4600022899/numpy-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51faf345324db860b515d3f364eaa93d0e0551a88d6218a7d61286554d190d73", size = 14335748 }, + { url = "https://files.pythonhosted.org/packages/f2/d4/f999444e86986f3533e7151c272bd8186c55dda554284def18557e013a2a/numpy-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38efc1e56b73cc9b182fe55e56e63b044dd26a72128fd2fbd502f75555d92591", size = 16391057 }, + { url = "https://files.pythonhosted.org/packages/99/7b/85cef6a3ae1b19542b7afd97d0b296526b6ef9e3c43ea0c4d9c4404fb2d0/numpy-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:31b89fa67a8042e96715c68e071a1200c4e172f93b0fbe01a14c0ff3ff820fc8", size = 15556943 }, + { url = "https://files.pythonhosted.org/packages/69/7e/b83cc884c3508e91af78760f6b17ab46ad649831b1fa35acb3eb26d9e6d2/numpy-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4c86e2a209199ead7ee0af65e1d9992d1dce7e1f63c4b9a616500f93820658d0", size = 18180785 }, + { url = "https://files.pythonhosted.org/packages/b2/9f/eb4a9a38867de059dcd4b6e18d47c3867fbd3795d4c9557bb49278f94087/numpy-2.2.1-cp311-cp311-win32.whl", hash = "sha256:b34d87e8a3090ea626003f87f9392b3929a7bbf4104a05b6667348b6bd4bf1cd", size = 6568983 }, + { url = "https://files.pythonhosted.org/packages/6d/1e/be3b9f3073da2f8c7fa361fcdc231b548266b0781029fdbaf75eeab997fd/numpy-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:360137f8fb1b753c5cde3ac388597ad680eccbbbb3865ab65efea062c4a1fd16", size = 12917260 }, + { url = "https://files.pythonhosted.org/packages/62/12/b928871c570d4a87ab13d2cc19f8817f17e340d5481621930e76b80ffb7d/numpy-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:694f9e921a0c8f252980e85bce61ebbd07ed2b7d4fa72d0e4246f2f8aa6642ab", size = 20909861 }, + { url = "https://files.pythonhosted.org/packages/3d/c3/59df91ae1d8ad7c5e03efd63fd785dec62d96b0fe56d1f9ab600b55009af/numpy-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3683a8d166f2692664262fd4900f207791d005fb088d7fdb973cc8d663626faa", size = 14095776 }, + { url = "https://files.pythonhosted.org/packages/af/4e/8ed5868efc8e601fb69419644a280e9c482b75691466b73bfaab7d86922c/numpy-2.2.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:780077d95eafc2ccc3ced969db22377b3864e5b9a0ea5eb347cc93b3ea900315", size = 5126239 }, + { url = "https://files.pythonhosted.org/packages/1a/74/dd0bbe650d7bc0014b051f092f2de65e34a8155aabb1287698919d124d7f/numpy-2.2.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:55ba24ebe208344aa7a00e4482f65742969a039c2acfcb910bc6fcd776eb4355", size = 6659296 }, + { url = "https://files.pythonhosted.org/packages/7f/11/4ebd7a3f4a655764dc98481f97bd0a662fb340d1001be6050606be13e162/numpy-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b1d07b53b78bf84a96898c1bc139ad7f10fda7423f5fd158fd0f47ec5e01ac7", size = 14047121 }, + { url = "https://files.pythonhosted.org/packages/7f/a7/c1f1d978166eb6b98ad009503e4d93a8c1962d0eb14a885c352ee0276a54/numpy-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5062dc1a4e32a10dc2b8b13cedd58988261416e811c1dc4dbdea4f57eea61b0d", size = 16096599 }, + { url = "https://files.pythonhosted.org/packages/3d/6d/0e22afd5fcbb4d8d0091f3f46bf4e8906399c458d4293da23292c0ba5022/numpy-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fce4f615f8ca31b2e61aa0eb5865a21e14f5629515c9151850aa936c02a1ee51", size = 15243932 }, + { url = "https://files.pythonhosted.org/packages/03/39/e4e5832820131ba424092b9610d996b37e5557180f8e2d6aebb05c31ae54/numpy-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:67d4cda6fa6ffa073b08c8372aa5fa767ceb10c9a0587c707505a6d426f4e046", size = 17861032 }, + { url = "https://files.pythonhosted.org/packages/5f/8a/3794313acbf5e70df2d5c7d2aba8718676f8d054a05abe59e48417fb2981/numpy-2.2.1-cp312-cp312-win32.whl", hash = "sha256:32cb94448be47c500d2c7a95f93e2f21a01f1fd05dd2beea1ccd049bb6001cd2", size = 6274018 }, + { url = "https://files.pythonhosted.org/packages/17/c1/c31d3637f2641e25c7a19adf2ae822fdaf4ddd198b05d79a92a9ce7cb63e/numpy-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:ba5511d8f31c033a5fcbda22dd5c813630af98c70b2661f2d2c654ae3cdfcfc8", size = 12613843 }, + { url = "https://files.pythonhosted.org/packages/20/d6/91a26e671c396e0c10e327b763485ee295f5a5a7a48c553f18417e5a0ed5/numpy-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f1d09e520217618e76396377c81fba6f290d5f926f50c35f3a5f72b01a0da780", size = 20896464 }, + { url = "https://files.pythonhosted.org/packages/8c/40/5792ccccd91d45e87d9e00033abc4f6ca8a828467b193f711139ff1f1cd9/numpy-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ecc47cd7f6ea0336042be87d9e7da378e5c7e9b3c8ad0f7c966f714fc10d821", size = 14111350 }, + { url = "https://files.pythonhosted.org/packages/c0/2a/fb0a27f846cb857cef0c4c92bef89f133a3a1abb4e16bba1c4dace2e9b49/numpy-2.2.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f419290bc8968a46c4933158c91a0012b7a99bb2e465d5ef5293879742f8797e", size = 5111629 }, + { url = "https://files.pythonhosted.org/packages/eb/e5/8e81bb9d84db88b047baf4e8b681a3e48d6390bc4d4e4453eca428ecbb49/numpy-2.2.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5b6c390bfaef8c45a260554888966618328d30e72173697e5cabe6b285fb2348", size = 6645865 }, + { url = "https://files.pythonhosted.org/packages/7a/1a/a90ceb191dd2f9e2897c69dde93ccc2d57dd21ce2acbd7b0333e8eea4e8d/numpy-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:526fc406ab991a340744aad7e25251dd47a6720a685fa3331e5c59fef5282a59", size = 14043508 }, + { url = "https://files.pythonhosted.org/packages/f1/5a/e572284c86a59dec0871a49cd4e5351e20b9c751399d5f1d79628c0542cb/numpy-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f74e6fdeb9a265624ec3a3918430205dff1df7e95a230779746a6af78bc615af", size = 16094100 }, + { url = "https://files.pythonhosted.org/packages/0c/2c/a79d24f364788386d85899dd280a94f30b0950be4b4a545f4fa4ed1d4ca7/numpy-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:53c09385ff0b72ba79d8715683c1168c12e0b6e84fb0372e97553d1ea91efe51", size = 15239691 }, + { url = "https://files.pythonhosted.org/packages/cf/79/1e20fd1c9ce5a932111f964b544facc5bb9bde7865f5b42f00b4a6a9192b/numpy-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f3eac17d9ec51be534685ba877b6ab5edc3ab7ec95c8f163e5d7b39859524716", size = 17856571 }, + { url = "https://files.pythonhosted.org/packages/be/5b/cc155e107f75d694f562bdc84a26cc930569f3dfdfbccb3420b626065777/numpy-2.2.1-cp313-cp313-win32.whl", hash = "sha256:9ad014faa93dbb52c80d8f4d3dcf855865c876c9660cb9bd7553843dd03a4b1e", size = 6270841 }, + { url = "https://files.pythonhosted.org/packages/44/be/0e5cd009d2162e4138d79a5afb3b5d2341f0fe4777ab6e675aa3d4a42e21/numpy-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:164a829b6aacf79ca47ba4814b130c4020b202522a93d7bff2202bfb33b61c60", size = 12606618 }, + { url = "https://files.pythonhosted.org/packages/a8/87/04ddf02dd86fb17c7485a5f87b605c4437966d53de1e3745d450343a6f56/numpy-2.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4dfda918a13cc4f81e9118dea249e192ab167a0bb1966272d5503e39234d694e", size = 20921004 }, + { url = "https://files.pythonhosted.org/packages/6e/3e/d0e9e32ab14005425d180ef950badf31b862f3839c5b927796648b11f88a/numpy-2.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:733585f9f4b62e9b3528dd1070ec4f52b8acf64215b60a845fa13ebd73cd0712", size = 14119910 }, + { url = "https://files.pythonhosted.org/packages/b5/5b/aa2d1905b04a8fb681e08742bb79a7bddfc160c7ce8e1ff6d5c821be0236/numpy-2.2.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:89b16a18e7bba224ce5114db863e7029803c179979e1af6ad6a6b11f70545008", size = 5153612 }, + { url = "https://files.pythonhosted.org/packages/ce/35/6831808028df0648d9b43c5df7e1051129aa0d562525bacb70019c5f5030/numpy-2.2.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:676f4eebf6b2d430300f1f4f4c2461685f8269f94c89698d832cdf9277f30b84", size = 6668401 }, + { url = "https://files.pythonhosted.org/packages/b1/38/10ef509ad63a5946cc042f98d838daebfe7eaf45b9daaf13df2086b15ff9/numpy-2.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f5cdf9f493b35f7e41e8368e7d7b4bbafaf9660cba53fb21d2cd174ec09631", size = 14014198 }, + { url = "https://files.pythonhosted.org/packages/df/f8/c80968ae01df23e249ee0a4487fae55a4c0fe2f838dfe9cc907aa8aea0fa/numpy-2.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1ad395cf254c4fbb5b2132fee391f361a6e8c1adbd28f2cd8e79308a615fe9d", size = 16076211 }, + { url = "https://files.pythonhosted.org/packages/09/69/05c169376016a0b614b432967ac46ff14269eaffab80040ec03ae1ae8e2c/numpy-2.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:08ef779aed40dbc52729d6ffe7dd51df85796a702afbf68a4f4e41fafdc8bda5", size = 15220266 }, + { url = "https://files.pythonhosted.org/packages/f1/ff/94a4ce67ea909f41cf7ea712aebbe832dc67decad22944a1020bb398a5ee/numpy-2.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:26c9c4382b19fcfbbed3238a14abf7ff223890ea1936b8890f058e7ba35e8d71", size = 17852844 }, + { url = "https://files.pythonhosted.org/packages/46/72/8a5dbce4020dfc595592333ef2fbb0a187d084ca243b67766d29d03e0096/numpy-2.2.1-cp313-cp313t-win32.whl", hash = "sha256:93cf4e045bae74c90ca833cba583c14b62cb4ba2cba0abd2b141ab52548247e2", size = 6326007 }, + { url = "https://files.pythonhosted.org/packages/7b/9c/4fce9cf39dde2562584e4cfd351a0140240f82c0e3569ce25a250f47037d/numpy-2.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:bff7d8ec20f5f42607599f9994770fa65d76edca264a87b5e4ea5629bce12268", size = 12693107 }, + { url = "https://files.pythonhosted.org/packages/f1/65/d36a76b811ffe0a4515e290cb05cb0e22171b1b0f0db6bee9141cf023545/numpy-2.2.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7ba9cc93a91d86365a5d270dee221fdc04fb68d7478e6bf6af650de78a8339e3", size = 21044672 }, + { url = "https://files.pythonhosted.org/packages/aa/3f/b644199f165063154df486d95198d814578f13dd4d8c1651e075bf1cb8af/numpy-2.2.1-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:3d03883435a19794e41f147612a77a8f56d4e52822337844fff3d4040a142964", size = 6789873 }, + { url = "https://files.pythonhosted.org/packages/d7/df/2adb0bb98a3cbe8a6c3c6d1019aede1f1d8b83927ced228a46cc56c7a206/numpy-2.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4511d9e6071452b944207c8ce46ad2f897307910b402ea5fa975da32e0102800", size = 16194933 }, + { url = "https://files.pythonhosted.org/packages/13/3e/1959d5219a9e6d200638d924cedda6a606392f7186a4ed56478252e70d55/numpy-2.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5c5cc0cbabe9452038ed984d05ac87910f89370b9242371bd9079cb4af61811e", size = 12820057 }, +] + [[package]] name = "packaging" version = "24.2" @@ -758,18 +878,18 @@ wheels = [ [[package]] name = "protobuf" -version = "5.29.2" +version = "5.29.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a5/73/4e6295c1420a9d20c9c351db3a36109b4c9aa601916cb7c6871e3196a1ca/protobuf-5.29.2.tar.gz", hash = "sha256:b2cc8e8bb7c9326996f0e160137b0861f1a82162502658df2951209d0cb0309e", size = 424901 } +sdist = { url = "https://files.pythonhosted.org/packages/f7/d1/e0a911544ca9993e0f17ce6d3cc0932752356c1b0a834397f28e63479344/protobuf-5.29.3.tar.gz", hash = "sha256:5da0f41edaf117bde316404bad1a486cb4ededf8e4a54891296f648e8e076620", size = 424945 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/42/6db5387124708d619ffb990a846fb123bee546f52868039f8fa964c5bc54/protobuf-5.29.2-cp310-abi3-win32.whl", hash = "sha256:c12ba8249f5624300cf51c3d0bfe5be71a60c63e4dcf51ffe9a68771d958c851", size = 422697 }, - { url = "https://files.pythonhosted.org/packages/6c/38/2fcc968b377b531882d6ab2ac99b10ca6d00108394f6ff57c2395fb7baff/protobuf-5.29.2-cp310-abi3-win_amd64.whl", hash = "sha256:842de6d9241134a973aab719ab42b008a18a90f9f07f06ba480df268f86432f9", size = 434495 }, - { url = "https://files.pythonhosted.org/packages/cb/26/41debe0f6615fcb7e97672057524687ed86fcd85e3da3f031c30af8f0c51/protobuf-5.29.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a0c53d78383c851bfa97eb42e3703aefdc96d2036a41482ffd55dc5f529466eb", size = 417812 }, - { url = "https://files.pythonhosted.org/packages/e4/20/38fc33b60dcfb380507b99494aebe8c34b68b8ac7d32808c4cebda3f6f6b/protobuf-5.29.2-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:494229ecd8c9009dd71eda5fd57528395d1eacdf307dbece6c12ad0dd09e912e", size = 319562 }, - { url = "https://files.pythonhosted.org/packages/90/4d/c3d61e698e0e41d926dbff6aa4e57428ab1a6fc3b5e1deaa6c9ec0fd45cf/protobuf-5.29.2-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:b6b0d416bbbb9d4fbf9d0561dbfc4e324fd522f61f7af0fe0f282ab67b22477e", size = 319662 }, - { url = "https://files.pythonhosted.org/packages/5e/d0/76d086c744c8252b35c2bc9c49c3be7c815b806191e58ad82c6d228c07a8/protobuf-5.29.2-cp39-cp39-win32.whl", hash = "sha256:36000f97ea1e76e8398a3f02936aac2a5d2b111aae9920ec1b769fc4a222c4d9", size = 422665 }, - { url = "https://files.pythonhosted.org/packages/84/08/be8223de1967ae8a100aaa1f7076f65c42ed1ff5ed413ff5dd718cff9fa8/protobuf-5.29.2-cp39-cp39-win_amd64.whl", hash = "sha256:2d2e674c58a06311c8e99e74be43e7f3a8d1e2b2fdf845eaa347fbd866f23355", size = 434584 }, - { url = "https://files.pythonhosted.org/packages/f3/fd/c7924b4c2a1c61b8f4b64edd7a31ffacf63432135a2606f03a2f0d75a750/protobuf-5.29.2-py3-none-any.whl", hash = "sha256:fde4554c0e578a5a0bcc9a276339594848d1e89f9ea47b4427c80e5d72f90181", size = 172539 }, + { url = "https://files.pythonhosted.org/packages/dc/7a/1e38f3cafa022f477ca0f57a1f49962f21ad25850c3ca0acd3b9d0091518/protobuf-5.29.3-cp310-abi3-win32.whl", hash = "sha256:3ea51771449e1035f26069c4c7fd51fba990d07bc55ba80701c78f886bf9c888", size = 422708 }, + { url = "https://files.pythonhosted.org/packages/61/fa/aae8e10512b83de633f2646506a6d835b151edf4b30d18d73afd01447253/protobuf-5.29.3-cp310-abi3-win_amd64.whl", hash = "sha256:a4fa6f80816a9a0678429e84973f2f98cbc218cca434abe8db2ad0bffc98503a", size = 434508 }, + { url = "https://files.pythonhosted.org/packages/dd/04/3eaedc2ba17a088961d0e3bd396eac764450f431621b58a04ce898acd126/protobuf-5.29.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a8434404bbf139aa9e1300dbf989667a83d42ddda9153d8ab76e0d5dcaca484e", size = 417825 }, + { url = "https://files.pythonhosted.org/packages/4f/06/7c467744d23c3979ce250397e26d8ad8eeb2bea7b18ca12ad58313c1b8d5/protobuf-5.29.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:daaf63f70f25e8689c072cfad4334ca0ac1d1e05a92fc15c54eb9cf23c3efd84", size = 319573 }, + { url = "https://files.pythonhosted.org/packages/a8/45/2ebbde52ad2be18d3675b6bee50e68cd73c9e0654de77d595540b5129df8/protobuf-5.29.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:c027e08a08be10b67c06bf2370b99c811c466398c357e615ca88c91c07f0910f", size = 319672 }, + { url = "https://files.pythonhosted.org/packages/85/a6/bf65a38f8be5ab8c3b575822acfd338702fdf7ac9abd8c81630cc7c9f4bd/protobuf-5.29.3-cp39-cp39-win32.whl", hash = "sha256:0eb32bfa5219fc8d4111803e9a690658aa2e6366384fd0851064b963b6d1f2a7", size = 422676 }, + { url = "https://files.pythonhosted.org/packages/ac/e2/48d46adc86369ff092eaece3e537f76b3baaab45ca3dde257838cde831d2/protobuf-5.29.3-cp39-cp39-win_amd64.whl", hash = "sha256:6ce8cc3389a20693bfde6c6562e03474c40851b44975c9b2bf6df7d8c4f864da", size = 434593 }, + { url = "https://files.pythonhosted.org/packages/fd/b2/ab07b09e0f6d143dfb839693aa05765257bceaa13d03bf1a696b78323e7a/protobuf-5.29.3-py3-none-any.whl", hash = "sha256:0a18ed4a24198528f2333802eb075e59dea9d679ab7a6c5efb017a59004d849f", size = 172550 }, ] [[package]] @@ -783,16 +903,16 @@ wheels = [ [[package]] name = "pydantic" -version = "2.10.4" +version = "2.10.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, { name = "pydantic-core" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/70/7e/fb60e6fee04d0ef8f15e4e01ff187a196fa976eb0f0ab524af4599e5754c/pydantic-2.10.4.tar.gz", hash = "sha256:82f12e9723da6de4fe2ba888b5971157b3be7ad914267dea8f05f82b28254f06", size = 762094 } +sdist = { url = "https://files.pythonhosted.org/packages/6a/c7/ca334c2ef6f2e046b1144fe4bb2a5da8a4c574e7f2ebf7e16b34a6a2fa92/pydantic-2.10.5.tar.gz", hash = "sha256:278b38dbbaec562011d659ee05f63346951b3a248a6f3642e1bc68894ea2b4ff", size = 761287 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/26/3e1bbe954fde7ee22a6e7d31582c642aad9e84ffe4b5fb61e63b87cd326f/pydantic-2.10.4-py3-none-any.whl", hash = "sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d", size = 431765 }, + { url = "https://files.pythonhosted.org/packages/58/26/82663c79010b28eddf29dcdd0ea723439535fa917fce5905885c0e9ba562/pydantic-2.10.5-py3-none-any.whl", hash = "sha256:4dd4e322dbe55472cb7ca7e73f4b63574eecccf2835ffa2af9021ce113c83c53", size = 431426 }, ] [[package]] @@ -911,11 +1031,11 @@ wheels = [ [[package]] name = "pygments" -version = "2.18.0" +version = "2.19.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, ] [[package]] @@ -967,7 +1087,7 @@ wheels = [ [[package]] name = "pysparkplug" -version = "0.4.1.dev0" +version = "0.4.1.dev11" source = { editable = "." } dependencies = [ { name = "paho-mqtt" }, @@ -982,10 +1102,13 @@ dev = [ { name = "furo" }, { name = "myst-parser", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "myst-parser", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numpy", version = "2.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "packaging" }, { name = "pygithub" }, { name = "pyright" }, { name = "pytest" }, + { name = "pytest-subtests" }, { name = "ruff" }, { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, @@ -1008,10 +1131,12 @@ dev = [ { name = "coverage", specifier = ">=7.6.10" }, { name = "furo", specifier = ">=2024.8.6" }, { name = "myst-parser", specifier = ">=3.0.1" }, + { name = "numpy", specifier = ">=2.0.2" }, { name = "packaging", specifier = ">=24.2" }, { name = "pygithub", specifier = ">=2.5.0" }, { name = "pyright", specifier = ">=1.1.391" }, { name = "pytest", specifier = ">=8.3.4" }, + { name = "pytest-subtests", specifier = ">=0.14.1" }, { name = "ruff", specifier = ">=0.8.5" }, { name = "sphinx", specifier = ">=7.4.7" }, { name = "sphinx-copybutton", specifier = ">=0.5.2" }, @@ -1037,6 +1162,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, ] +[[package]] +name = "pytest-subtests" +version = "0.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/4c/ba9eab21a2250c2d46c06c0e3cd316850fde9a90da0ac8d0202f074c6817/pytest_subtests-0.14.1.tar.gz", hash = "sha256:350c00adc36c3aff676a66135c81aed9e2182e15f6c3ec8721366918bbbf7580", size = 17632 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/b7/7ca948d35642ae72500efda6ba6fa61dcb6683feb596d19c4747c63c0789/pytest_subtests-0.14.1-py3-none-any.whl", hash = "sha256:e92a780d98b43118c28a16044ad9b841727bd7cb6a417073b38fd2d7ccdf052d", size = 8833 }, +] + [[package]] name = "pywin32-ctypes" version = "0.2.3" @@ -1165,27 +1303,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.8.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/25/5d/4b5403f3e89837decfd54c51bea7f94b7d3fae77e08858603d0e04d7ad17/ruff-0.8.5.tar.gz", hash = "sha256:1098d36f69831f7ff2a1da3e6407d5fbd6dfa2559e4f74ff2d260c5588900317", size = 3454835 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/73/f8/03391745a703ce11678eb37c48ae89ec60396ea821e9d0bcea7c8e88fd91/ruff-0.8.5-py3-none-linux_armv6l.whl", hash = "sha256:5ad11a5e3868a73ca1fa4727fe7e33735ea78b416313f4368c504dbeb69c0f88", size = 10626889 }, - { url = "https://files.pythonhosted.org/packages/55/74/83bb74a44183b904216f3edfb9995b89830c83aaa6ce84627f74da0e0cf8/ruff-0.8.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f69ab37771ea7e0715fead8624ec42996d101269a96e31f4d31be6fc33aa19b7", size = 10398233 }, - { url = "https://files.pythonhosted.org/packages/e8/7a/a162a4feb3ef85d594527165e366dde09d7a1e534186ff4ba5d127eda850/ruff-0.8.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b5462d7804558ccff9c08fe8cbf6c14b7efe67404316696a2dde48297b1925bb", size = 10001843 }, - { url = "https://files.pythonhosted.org/packages/e7/9f/5ee5dcd135411402e35b6ec6a8dfdadbd31c5cd1c36a624d356a38d76090/ruff-0.8.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d56de7220a35607f9fe59f8a6d018e14504f7b71d784d980835e20fc0611cd50", size = 10872507 }, - { url = "https://files.pythonhosted.org/packages/b6/67/db2df2dd4a34b602d7f6ebb1b3744c8157f0d3579973ffc58309c9c272e8/ruff-0.8.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9d99cf80b0429cbebf31cbbf6f24f05a29706f0437c40413d950e67e2d4faca4", size = 10377200 }, - { url = "https://files.pythonhosted.org/packages/fe/ff/fe3a6a73006bced73e60d171d154a82430f61d97e787f511a24bd6302611/ruff-0.8.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b75ac29715ac60d554a049dbb0ef3b55259076181c3369d79466cb130eb5afd", size = 11433155 }, - { url = "https://files.pythonhosted.org/packages/e3/95/c1d1a1fe36658c1f3e1b47e1cd5f688b72d5786695b9e621c2c38399a95e/ruff-0.8.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c9d526a62c9eda211b38463528768fd0ada25dad524cb33c0e99fcff1c67b5dc", size = 12139227 }, - { url = "https://files.pythonhosted.org/packages/1b/fe/644b70d473a27b5112ac7a3428edcc1ce0db775c301ff11aa146f71886e0/ruff-0.8.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:587c5e95007612c26509f30acc506c874dab4c4abbacd0357400bd1aa799931b", size = 11697941 }, - { url = "https://files.pythonhosted.org/packages/00/39/4f83e517ec173e16a47c6d102cd22a1aaebe80e1208a1f2e83ab9a0e4134/ruff-0.8.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:622b82bf3429ff0e346835ec213aec0a04d9730480cbffbb6ad9372014e31bbd", size = 12967686 }, - { url = "https://files.pythonhosted.org/packages/1a/f6/52a2973ff108d74b5da706a573379eea160bece098f7cfa3f35dc4622710/ruff-0.8.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f99be814d77a5dac8a8957104bdd8c359e85c86b0ee0e38dca447cb1095f70fb", size = 11253788 }, - { url = "https://files.pythonhosted.org/packages/ce/1f/3b30f3c65b1303cb8e268ec3b046b77ab21ed8e26921cfc7e8232aa57f2c/ruff-0.8.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c01c048f9c3385e0fd7822ad0fd519afb282af9cf1778f3580e540629df89725", size = 10860360 }, - { url = "https://files.pythonhosted.org/packages/a5/a8/2a3ea6bacead963f7aeeba0c61815d9b27b0d638e6a74984aa5cc5d27733/ruff-0.8.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7512e8cb038db7f5db6aae0e24735ff9ea03bb0ed6ae2ce534e9baa23c1dc9ea", size = 10457922 }, - { url = "https://files.pythonhosted.org/packages/17/47/8f9514b670969aab57c5fc826fb500a16aee8feac1bcf8a91358f153a5ba/ruff-0.8.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:762f113232acd5b768d6b875d16aad6b00082add40ec91c927f0673a8ec4ede8", size = 10958347 }, - { url = "https://files.pythonhosted.org/packages/0d/d6/78a9af8209ad99541816d74f01ce678fc01ebb3f37dd7ab8966646dcd92b/ruff-0.8.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:03a90200c5dfff49e4c967b405f27fdfa81594cbb7c5ff5609e42d7fe9680da5", size = 11328882 }, - { url = "https://files.pythonhosted.org/packages/54/77/5c8072ec7afdfdf42c7a4019044486a2b6c85ee73617f8875ec94b977fed/ruff-0.8.5-py3-none-win32.whl", hash = "sha256:8710ffd57bdaa6690cbf6ecff19884b8629ec2a2a2a2f783aa94b1cc795139ed", size = 8802515 }, - { url = "https://files.pythonhosted.org/packages/bc/b6/47d2b06784de8ae992c45cceb2a30f3f205b3236a629d7ca4c0c134839a2/ruff-0.8.5-py3-none-win_amd64.whl", hash = "sha256:4020d8bf8d3a32325c77af452a9976a9ad6455773bcb94991cf15bd66b347e47", size = 9684231 }, - { url = "https://files.pythonhosted.org/packages/bf/5e/ffee22bf9f9e4b2669d1f0179ae8804584939fb6502b51f2401e26b1e028/ruff-0.8.5-py3-none-win_arm64.whl", hash = "sha256:134ae019ef13e1b060ab7136e7828a6d83ea727ba123381307eb37c6bd5e01cb", size = 9124741 }, +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/48/385f276f41e89623a5ea8e4eb9c619a44fdfc2a64849916b3584eca6cb9f/ruff-0.9.0.tar.gz", hash = "sha256:143f68fa5560ecf10fc49878b73cee3eab98b777fcf43b0e62d43d42f5ef9d8b", size = 3489167 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/01/e0885e5519212efc7ab9d868bc39cb9781931c4c6f9b17becafa81193ec4/ruff-0.9.0-py3-none-linux_armv6l.whl", hash = "sha256:949b3513f931741e006cf267bf89611edff04e1f012013424022add3ce78f319", size = 10647069 }, + { url = "https://files.pythonhosted.org/packages/dd/69/510a9a5781dcf84c2ad513c2003936fefc802f39c745d5f2355d77fa45fd/ruff-0.9.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:99fbcb8c7fe94ae1e462ab2a1ef17cb20b25fb6438b9f198b1bcf5207a0a7916", size = 10401936 }, + { url = "https://files.pythonhosted.org/packages/07/9f/37fb86bfdf28c4cbfe94cbcc01fb9ab0cb8128548f243f34d5298b212562/ruff-0.9.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0b022afd8eb0fcfce1e0adec84322abf4d6ce3cd285b3b99c4f17aae7decf749", size = 10010347 }, + { url = "https://files.pythonhosted.org/packages/30/0d/b95121f53c7f7bfb7ba427a35d25f983ed3b476620c5cd69f45caa5b294e/ruff-0.9.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:336567ce92c9ca8ec62780d07b5fa11fbc881dc7bb40958f93a7d621e7ab4589", size = 10882152 }, + { url = "https://files.pythonhosted.org/packages/d4/0b/a955cb6b19eb900c4c594707ab72132ce2d5cd8b5565137fb8fed21b8f08/ruff-0.9.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d338336c44bda602dc8e8766836ac0441e5b0dfeac3af1bd311a97ebaf087a75", size = 10405502 }, + { url = "https://files.pythonhosted.org/packages/1e/fa/9a6c70af74f20edd2519b89eb3322f4bfa399315cf306383443700f2d6b6/ruff-0.9.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9b3ececf523d733e90b540e7afcc0494189e8999847f8855747acd5a9a8c45f", size = 11465069 }, + { url = "https://files.pythonhosted.org/packages/ee/8b/7effac8915470da496be009fe861060baff2692f92801976b2c01cdc8c54/ruff-0.9.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a11c0872a31232e473e2e0e2107f3d294dbadd2f83fb281c3eb1c22a24866924", size = 12176850 }, + { url = "https://files.pythonhosted.org/packages/bd/ed/626179786889eca47b1e821c1582622ac0c1c8f01d60ac974f8b96867a57/ruff-0.9.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b5fd06220c17a9cc0dc7fc6552f2ac4db74e8e8bff9c401d160ac59d00566f54", size = 11700963 }, + { url = "https://files.pythonhosted.org/packages/75/79/094c34ddec47fd3c61a0bc5e83ca164344c592949cff91f05961fd40922e/ruff-0.9.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0457e775c74bf3976243f910805242b7dcd389e1d440deccbd1194ca17a5728c", size = 13096560 }, + { url = "https://files.pythonhosted.org/packages/e7/23/ec85dca0dcb329835197401734501bfa1d39e72343df64628c67b72bcbf5/ruff-0.9.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05415599bbcb318f730ea1b46a39e4fbf71f6a63fdbfa1dda92efb55f19d7ecf", size = 11278658 }, + { url = "https://files.pythonhosted.org/packages/6c/17/1b3ea5f06578ea1daa08ac35f9de099d1827eea6e116a8cabbf11235c925/ruff-0.9.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fbf9864b009e43cfc1c8bed1a6a4c529156913105780af4141ca4342148517f5", size = 10879847 }, + { url = "https://files.pythonhosted.org/packages/a6/e5/00bc97d6f419da03c0d898e95cca77311494e7274dc7cc17d94976e32e52/ruff-0.9.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:37b3da222b12e2bb2ce628e02586ab4846b1ed7f31f42a5a0683b213453b2d49", size = 10494220 }, + { url = "https://files.pythonhosted.org/packages/cc/70/d0a23d94f3e40b7ffac0e5506f33bb504672569173781a6c7cab0db6a4ba/ruff-0.9.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:733c0fcf2eb0c90055100b4ed1af9c9d87305b901a8feb6a0451fa53ed88199d", size = 11004182 }, + { url = "https://files.pythonhosted.org/packages/20/8e/367cf8e401890f823d0e4eb33635d0113719d5660b6522b7295376dd95fd/ruff-0.9.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8221a454bfe5ccdf8017512fd6bb60e6ec30f9ea252b8a80e5b73619f6c3cefd", size = 11345761 }, + { url = "https://files.pythonhosted.org/packages/fe/08/4b54e02da73060ebc29368ab15868613f7d2496bde3b01d284d5423646bc/ruff-0.9.0-py3-none-win32.whl", hash = "sha256:d345f2178afd192c7991ddee59155c58145e12ad81310b509bd2e25c5b0247b3", size = 8807005 }, + { url = "https://files.pythonhosted.org/packages/a1/a7/0b422971e897c51bf805f998d75bcfe5d4d858f5002203832875fc91b733/ruff-0.9.0-py3-none-win_amd64.whl", hash = "sha256:0cbc0905d94d21305872f7f8224e30f4bbcd532bc21b2225b2446d8fc7220d19", size = 9689974 }, + { url = "https://files.pythonhosted.org/packages/73/0e/c00f66731e514be3299801b1d9d54efae0abfe8f00a5c14155f2ab9e2920/ruff-0.9.0-py3-none-win_arm64.whl", hash = "sha256:7b1148771c6ca88f820d761350a053a5794bc58e0867739ea93eb5e41ad978cd", size = 9147729 }, ] [[package]]