From e8a63618fe9db580f710d3d6fdf704ec0795bc8c Mon Sep 17 00:00:00 2001 From: marcosf2 Date: Wed, 11 Feb 2026 11:25:33 -0600 Subject: [PATCH 01/12] Moving everything to src directory --- pyproject.toml | 38 + setup.py | 28 - .../instrumentserver}/__init__.py | 0 .../instrumentserver}/apps.py | 0 .../instrumentserver}/base.py | 0 .../instrumentserver}/blueprints.py | 0 .../instrumentserver}/client/__init__.py | 0 .../instrumentserver}/client/application.py | 0 .../instrumentserver}/client/core.py | 0 .../instrumentserver}/client/proxy.py | 0 .../instrumentserver}/config.py | 0 .../instrumentserver}/deployment/Dockerfile | 0 .../instrumentserver}/deployment/README.md | 0 .../deployment/dashboard.json | 0 .../deployment/docker-compose.yml | 0 .../instrumentserver}/deployment/grafana.ini | 0 .../datasources/csvdatasource.yml | 0 .../instrumentserver}/gui/__init__.py | 0 .../instrumentserver}/gui/base_instrument.py | 0 .../instrumentserver}/gui/instruments.py | 0 .../instrumentserver}/gui/misc.py | 0 .../instrumentserver}/gui/parameters.py | 0 .../instrumentserver}/helpers.py | 0 .../instrumentserver}/log.py | 0 .../instrumentserver}/monitoring/listener.py | 0 .../instrumentserver}/params.py | 0 .../instrumentserver}/resource.py | 2946 ++++++++--------- .../instrumentserver}/resource.qrc | 0 .../resource/icons/alert-octagon-green.svg | 0 .../resource/icons/alert-octagon-red.svg | 0 .../resource/icons/alert-octagon.svg | 0 .../resource/icons/client_app_icon.svg | 0 .../instrumentserver}/resource/icons/code.svg | 0 .../resource/icons/collapse.svg | 0 .../resource/icons/delete.svg | 0 .../resource/icons/expand.svg | 0 .../resource/icons/folder.svg | 0 .../instrumentserver}/resource/icons/load.svg | 0 .../resource/icons/plus-square.svg | 0 .../resource/icons/python.svg | 0 .../resource/icons/refresh.svg | 0 .../instrumentserver}/resource/icons/save.svg | 0 .../resource/icons/server_app_icon.svg | 0 .../instrumentserver}/resource/icons/set.svg | 0 .../resource/icons/star-crossed.svg | 0 .../instrumentserver}/resource/icons/star.svg | 0 .../resource/icons/trash-crossed.svg | 0 .../resource/icons/trash.svg | 0 .../instrumentserver}/resource/style.css | 0 .../schemas/instruction_dict.json | 0 .../instrumentserver}/schemas/parameters.json | 0 .../instrumentserver}/serialize.py | 0 .../instrumentserver}/server/__init__.py | 0 .../instrumentserver}/server/application.py | 0 .../instrumentserver}/server/core.py | 0 .../instrumentserver}/server/pollingWorker.py | 0 .../instrumentserver}/testing/__init__.py | 0 .../testing/create_instrument.py | 0 .../testing/dummy_instruments/__init__.py | 0 .../testing/dummy_instruments/generic.py | 0 .../testing/dummy_instruments/rf.py | 0 test/notebooks/Autoupdate.ipynb | 20 +- 62 files changed, 1521 insertions(+), 1511 deletions(-) create mode 100644 pyproject.toml delete mode 100644 setup.py rename {instrumentserver => src/instrumentserver}/__init__.py (100%) rename {instrumentserver => src/instrumentserver}/apps.py (100%) rename {instrumentserver => src/instrumentserver}/base.py (100%) rename {instrumentserver => src/instrumentserver}/blueprints.py (100%) rename {instrumentserver => src/instrumentserver}/client/__init__.py (100%) rename {instrumentserver => src/instrumentserver}/client/application.py (100%) rename {instrumentserver => src/instrumentserver}/client/core.py (100%) rename {instrumentserver => src/instrumentserver}/client/proxy.py (100%) rename {instrumentserver => src/instrumentserver}/config.py (100%) rename {instrumentserver => src/instrumentserver}/deployment/Dockerfile (100%) rename {instrumentserver => src/instrumentserver}/deployment/README.md (100%) rename {instrumentserver => src/instrumentserver}/deployment/dashboard.json (100%) rename {instrumentserver => src/instrumentserver}/deployment/docker-compose.yml (100%) rename {instrumentserver => src/instrumentserver}/deployment/grafana.ini (100%) rename {instrumentserver => src/instrumentserver}/deployment/provisioning/datasources/csvdatasource.yml (100%) rename {instrumentserver => src/instrumentserver}/gui/__init__.py (100%) rename {instrumentserver => src/instrumentserver}/gui/base_instrument.py (100%) rename {instrumentserver => src/instrumentserver}/gui/instruments.py (100%) rename {instrumentserver => src/instrumentserver}/gui/misc.py (100%) rename {instrumentserver => src/instrumentserver}/gui/parameters.py (100%) rename {instrumentserver => src/instrumentserver}/helpers.py (100%) rename {instrumentserver => src/instrumentserver}/log.py (100%) rename {instrumentserver => src/instrumentserver}/monitoring/listener.py (100%) rename {instrumentserver => src/instrumentserver}/params.py (100%) rename {instrumentserver => src/instrumentserver}/resource.py (98%) rename {instrumentserver => src/instrumentserver}/resource.qrc (100%) rename {instrumentserver => src/instrumentserver}/resource/icons/alert-octagon-green.svg (100%) rename {instrumentserver => src/instrumentserver}/resource/icons/alert-octagon-red.svg (100%) rename {instrumentserver => src/instrumentserver}/resource/icons/alert-octagon.svg (100%) rename {instrumentserver => src/instrumentserver}/resource/icons/client_app_icon.svg (100%) rename {instrumentserver => src/instrumentserver}/resource/icons/code.svg (100%) rename {instrumentserver => src/instrumentserver}/resource/icons/collapse.svg (100%) rename {instrumentserver => src/instrumentserver}/resource/icons/delete.svg (100%) rename {instrumentserver => src/instrumentserver}/resource/icons/expand.svg (100%) rename {instrumentserver => src/instrumentserver}/resource/icons/folder.svg (100%) rename {instrumentserver => src/instrumentserver}/resource/icons/load.svg (100%) rename {instrumentserver => src/instrumentserver}/resource/icons/plus-square.svg (100%) rename {instrumentserver => src/instrumentserver}/resource/icons/python.svg (100%) rename {instrumentserver => src/instrumentserver}/resource/icons/refresh.svg (100%) rename {instrumentserver => src/instrumentserver}/resource/icons/save.svg (100%) rename {instrumentserver => src/instrumentserver}/resource/icons/server_app_icon.svg (100%) rename {instrumentserver => src/instrumentserver}/resource/icons/set.svg (100%) rename {instrumentserver => src/instrumentserver}/resource/icons/star-crossed.svg (100%) rename {instrumentserver => src/instrumentserver}/resource/icons/star.svg (100%) rename {instrumentserver => src/instrumentserver}/resource/icons/trash-crossed.svg (100%) rename {instrumentserver => src/instrumentserver}/resource/icons/trash.svg (100%) rename {instrumentserver => src/instrumentserver}/resource/style.css (100%) rename {instrumentserver => src/instrumentserver}/schemas/instruction_dict.json (100%) rename {instrumentserver => src/instrumentserver}/schemas/parameters.json (100%) rename {instrumentserver => src/instrumentserver}/serialize.py (100%) rename {instrumentserver => src/instrumentserver}/server/__init__.py (100%) rename {instrumentserver => src/instrumentserver}/server/application.py (100%) rename {instrumentserver => src/instrumentserver}/server/core.py (100%) rename {instrumentserver => src/instrumentserver}/server/pollingWorker.py (100%) rename {instrumentserver => src/instrumentserver}/testing/__init__.py (100%) rename {instrumentserver => src/instrumentserver}/testing/create_instrument.py (100%) rename {instrumentserver => src/instrumentserver}/testing/dummy_instruments/__init__.py (100%) rename {instrumentserver => src/instrumentserver}/testing/dummy_instruments/generic.py (100%) rename {instrumentserver => src/instrumentserver}/testing/dummy_instruments/rf.py (100%) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e907b9c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,38 @@ +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "instrumentserver" +version = "0.0.1" +description = "Distributed instrument control server and client for QCoDeS." +readme = "README.md" +license = {file = "LICENSE"} +requires-python = ">=3.11" +authors = [ + {name = "Wolfgang Pfaff", email = "wolfgangpfff@gmail.com"}, + {name = "Marcos Frenkel", email = "marcosf2@illinois.edu"} +] +dependencies = [ + "pyqt5", + "pyzmq", + "qcodes", + "qtpy", + "scipy" +] + +[project.urls] +Homepage = "https://github.com/toolsforexperiments/instrumentserver" + +[project.scripts] +instrumentserver = "instrumentserver.apps:serverScript" +instrumentserver-client-station = "instrumentserver.apps:clientStationScript" +instrumentserver-detached = "instrumentserver.apps:detachedServerScript" +instrumentserver-listener = "instrumentserver.monitoring.listener:startListener" +instrumentserver-param-manager = "instrumentserver.apps:parameterManagerScript" + +[tool.setuptools] +package-dir = {"" = "src"} + +[tool.setuptools.packages.find] +where = ["src"] diff --git a/setup.py b/setup.py deleted file mode 100644 index b32c990..0000000 --- a/setup.py +++ /dev/null @@ -1,28 +0,0 @@ -from setuptools import setup - -setup(name='instrumentserver', - version='0.0.1', - description='TBD', - url='https://github.com/toolsforexperiments/instrumentserver', - author='Wolfgang Pfaff', - author_email='wolfgangpfff@gmail.com', - license='MIT', - packages=['instrumentserver'], - zip_safe=False, - entry_points={ - "console_scripts": [ - "instrumentserver = instrumentserver.apps:serverScript", - "instrumentserver-detached = instrumentserver.apps:detachedServerScript", - "instrumentserver-client-station = instrumentserver.apps:clientStationScript", - "instrumentserver-param-manager = instrumentserver.apps:parameterManagerScript", - "instrumentserver-listener = instrumentserver.monitoring.listener:startListener", - ]}, - install_requires = [ - 'pyzmq', - 'qcodes', - 'qtpy', - 'pyqt5', - 'bokeh', - 'scipy' - ] - ) diff --git a/instrumentserver/__init__.py b/src/instrumentserver/__init__.py similarity index 100% rename from instrumentserver/__init__.py rename to src/instrumentserver/__init__.py diff --git a/instrumentserver/apps.py b/src/instrumentserver/apps.py similarity index 100% rename from instrumentserver/apps.py rename to src/instrumentserver/apps.py diff --git a/instrumentserver/base.py b/src/instrumentserver/base.py similarity index 100% rename from instrumentserver/base.py rename to src/instrumentserver/base.py diff --git a/instrumentserver/blueprints.py b/src/instrumentserver/blueprints.py similarity index 100% rename from instrumentserver/blueprints.py rename to src/instrumentserver/blueprints.py diff --git a/instrumentserver/client/__init__.py b/src/instrumentserver/client/__init__.py similarity index 100% rename from instrumentserver/client/__init__.py rename to src/instrumentserver/client/__init__.py diff --git a/instrumentserver/client/application.py b/src/instrumentserver/client/application.py similarity index 100% rename from instrumentserver/client/application.py rename to src/instrumentserver/client/application.py diff --git a/instrumentserver/client/core.py b/src/instrumentserver/client/core.py similarity index 100% rename from instrumentserver/client/core.py rename to src/instrumentserver/client/core.py diff --git a/instrumentserver/client/proxy.py b/src/instrumentserver/client/proxy.py similarity index 100% rename from instrumentserver/client/proxy.py rename to src/instrumentserver/client/proxy.py diff --git a/instrumentserver/config.py b/src/instrumentserver/config.py similarity index 100% rename from instrumentserver/config.py rename to src/instrumentserver/config.py diff --git a/instrumentserver/deployment/Dockerfile b/src/instrumentserver/deployment/Dockerfile similarity index 100% rename from instrumentserver/deployment/Dockerfile rename to src/instrumentserver/deployment/Dockerfile diff --git a/instrumentserver/deployment/README.md b/src/instrumentserver/deployment/README.md similarity index 100% rename from instrumentserver/deployment/README.md rename to src/instrumentserver/deployment/README.md diff --git a/instrumentserver/deployment/dashboard.json b/src/instrumentserver/deployment/dashboard.json similarity index 100% rename from instrumentserver/deployment/dashboard.json rename to src/instrumentserver/deployment/dashboard.json diff --git a/instrumentserver/deployment/docker-compose.yml b/src/instrumentserver/deployment/docker-compose.yml similarity index 100% rename from instrumentserver/deployment/docker-compose.yml rename to src/instrumentserver/deployment/docker-compose.yml diff --git a/instrumentserver/deployment/grafana.ini b/src/instrumentserver/deployment/grafana.ini similarity index 100% rename from instrumentserver/deployment/grafana.ini rename to src/instrumentserver/deployment/grafana.ini diff --git a/instrumentserver/deployment/provisioning/datasources/csvdatasource.yml b/src/instrumentserver/deployment/provisioning/datasources/csvdatasource.yml similarity index 100% rename from instrumentserver/deployment/provisioning/datasources/csvdatasource.yml rename to src/instrumentserver/deployment/provisioning/datasources/csvdatasource.yml diff --git a/instrumentserver/gui/__init__.py b/src/instrumentserver/gui/__init__.py similarity index 100% rename from instrumentserver/gui/__init__.py rename to src/instrumentserver/gui/__init__.py diff --git a/instrumentserver/gui/base_instrument.py b/src/instrumentserver/gui/base_instrument.py similarity index 100% rename from instrumentserver/gui/base_instrument.py rename to src/instrumentserver/gui/base_instrument.py diff --git a/instrumentserver/gui/instruments.py b/src/instrumentserver/gui/instruments.py similarity index 100% rename from instrumentserver/gui/instruments.py rename to src/instrumentserver/gui/instruments.py diff --git a/instrumentserver/gui/misc.py b/src/instrumentserver/gui/misc.py similarity index 100% rename from instrumentserver/gui/misc.py rename to src/instrumentserver/gui/misc.py diff --git a/instrumentserver/gui/parameters.py b/src/instrumentserver/gui/parameters.py similarity index 100% rename from instrumentserver/gui/parameters.py rename to src/instrumentserver/gui/parameters.py diff --git a/instrumentserver/helpers.py b/src/instrumentserver/helpers.py similarity index 100% rename from instrumentserver/helpers.py rename to src/instrumentserver/helpers.py diff --git a/instrumentserver/log.py b/src/instrumentserver/log.py similarity index 100% rename from instrumentserver/log.py rename to src/instrumentserver/log.py diff --git a/instrumentserver/monitoring/listener.py b/src/instrumentserver/monitoring/listener.py similarity index 100% rename from instrumentserver/monitoring/listener.py rename to src/instrumentserver/monitoring/listener.py diff --git a/instrumentserver/params.py b/src/instrumentserver/params.py similarity index 100% rename from instrumentserver/params.py rename to src/instrumentserver/params.py diff --git a/instrumentserver/resource.py b/src/instrumentserver/resource.py similarity index 98% rename from instrumentserver/resource.py rename to src/instrumentserver/resource.py index b6611bd..b8f97f0 100644 --- a/instrumentserver/resource.py +++ b/src/instrumentserver/resource.py @@ -1,1473 +1,1473 @@ -# -*- coding: utf-8 -*- - -# Resource object code -# -# Created by: The Resource Compiler for PyQt5 (Qt v5.15.2) -# -# WARNING! All changes made in this file will be lost! - -from PyQt5 import QtCore - -qt_resource_data = b"\ -\x00\x00\x00\x00\ -\ -\x00\x00\x01\x75\ -\x3c\ -\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ -\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ -\x30\x2f\x73\x76\x67\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x32\x34\ -\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x32\x34\x22\x20\x76\x69\ -\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\x30\x20\x32\x34\x20\x32\x34\ -\x22\x20\x66\x69\x6c\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\x20\x73\x74\ -\x72\x6f\x6b\x65\x3d\x22\x63\x75\x72\x72\x65\x6e\x74\x43\x6f\x6c\ -\x6f\x72\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\x68\ -\x3d\x22\x32\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\ -\x63\x61\x70\x3d\x22\x72\x6f\x75\x6e\x64\x22\x20\x73\x74\x72\x6f\ -\x6b\x65\x2d\x6c\x69\x6e\x65\x6a\x6f\x69\x6e\x3d\x22\x72\x6f\x75\ -\x6e\x64\x22\x20\x63\x6c\x61\x73\x73\x3d\x22\x66\x65\x61\x74\x68\ -\x65\x72\x20\x66\x65\x61\x74\x68\x65\x72\x2d\x70\x6c\x75\x73\x2d\ -\x73\x71\x75\x61\x72\x65\x22\x3e\x3c\x72\x65\x63\x74\x20\x78\x3d\ -\x22\x33\x22\x20\x79\x3d\x22\x33\x22\x20\x77\x69\x64\x74\x68\x3d\ -\x22\x31\x38\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x31\x38\x22\ -\x20\x72\x78\x3d\x22\x32\x22\x20\x72\x79\x3d\x22\x32\x22\x3e\x3c\ -\x2f\x72\x65\x63\x74\x3e\x3c\x6c\x69\x6e\x65\x20\x78\x31\x3d\x22\ -\x31\x32\x22\x20\x79\x31\x3d\x22\x38\x22\x20\x78\x32\x3d\x22\x31\ -\x32\x22\x20\x79\x32\x3d\x22\x31\x36\x22\x3e\x3c\x2f\x6c\x69\x6e\ -\x65\x3e\x3c\x6c\x69\x6e\x65\x20\x78\x31\x3d\x22\x38\x22\x20\x79\ -\x31\x3d\x22\x31\x32\x22\x20\x78\x32\x3d\x22\x31\x36\x22\x20\x79\ -\x32\x3d\x22\x31\x32\x22\x3e\x3c\x2f\x6c\x69\x6e\x65\x3e\x3c\x2f\ -\x73\x76\x67\x3e\ -\x00\x00\x01\xa0\ -\x3c\ -\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ -\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ -\x30\x2f\x73\x76\x67\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x32\x34\ -\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x32\x34\x22\x20\x76\x69\ -\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\x30\x20\x32\x34\x20\x32\x34\ -\x22\x20\x66\x69\x6c\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\x20\x73\x74\ -\x72\x6f\x6b\x65\x3d\x22\x63\x75\x72\x72\x65\x6e\x74\x43\x6f\x6c\ -\x6f\x72\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\x68\ -\x3d\x22\x32\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\ -\x63\x61\x70\x3d\x22\x72\x6f\x75\x6e\x64\x22\x20\x73\x74\x72\x6f\ -\x6b\x65\x2d\x6c\x69\x6e\x65\x6a\x6f\x69\x6e\x3d\x22\x72\x6f\x75\ -\x6e\x64\x22\x20\x63\x6c\x61\x73\x73\x3d\x22\x66\x65\x61\x74\x68\ -\x65\x72\x20\x66\x65\x61\x74\x68\x65\x72\x2d\x61\x6c\x65\x72\x74\ -\x2d\x6f\x63\x74\x61\x67\x6f\x6e\x22\x3e\x3c\x70\x6f\x6c\x79\x67\ -\x6f\x6e\x20\x70\x6f\x69\x6e\x74\x73\x3d\x22\x37\x2e\x38\x36\x20\ -\x32\x20\x31\x36\x2e\x31\x34\x20\x32\x20\x32\x32\x20\x37\x2e\x38\ -\x36\x20\x32\x32\x20\x31\x36\x2e\x31\x34\x20\x31\x36\x2e\x31\x34\ -\x20\x32\x32\x20\x37\x2e\x38\x36\x20\x32\x32\x20\x32\x20\x31\x36\ -\x2e\x31\x34\x20\x32\x20\x37\x2e\x38\x36\x20\x37\x2e\x38\x36\x20\ -\x32\x22\x3e\x3c\x2f\x70\x6f\x6c\x79\x67\x6f\x6e\x3e\x3c\x6c\x69\ -\x6e\x65\x20\x78\x31\x3d\x22\x31\x32\x22\x20\x79\x31\x3d\x22\x38\ -\x22\x20\x78\x32\x3d\x22\x31\x32\x22\x20\x79\x32\x3d\x22\x31\x32\ -\x22\x3e\x3c\x2f\x6c\x69\x6e\x65\x3e\x3c\x6c\x69\x6e\x65\x20\x78\ -\x31\x3d\x22\x31\x32\x22\x20\x79\x31\x3d\x22\x31\x36\x22\x20\x78\ -\x32\x3d\x22\x31\x32\x2e\x30\x31\x22\x20\x79\x32\x3d\x22\x31\x36\ -\x22\x3e\x3c\x2f\x6c\x69\x6e\x65\x3e\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x07\xd9\ -\x3c\ -\x73\x76\x67\x20\x77\x69\x64\x74\x68\x3d\x22\x38\x30\x22\x20\x68\ -\x65\x69\x67\x68\x74\x3d\x22\x38\x30\x22\x20\x78\x6d\x6c\x6e\x73\ -\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\ -\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x20\x66\x69\ -\x6c\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\x3e\x0a\x20\x3c\x67\x3e\x0a\ -\x20\x20\x3c\x74\x69\x74\x6c\x65\x3e\x4c\x61\x79\x65\x72\x20\x31\ -\x3c\x2f\x74\x69\x74\x6c\x65\x3e\x0a\x20\x20\x3c\x70\x61\x74\x68\ -\x20\x64\x3d\x22\x6d\x33\x38\x2e\x31\x34\x34\x31\x2c\x31\x35\x2e\ -\x31\x36\x31\x36\x31\x63\x30\x2e\x36\x37\x31\x38\x2c\x2d\x31\x2e\ -\x36\x37\x32\x39\x20\x33\x2e\x30\x34\x2c\x2d\x31\x2e\x36\x37\x32\ -\x39\x20\x33\x2e\x37\x31\x31\x38\x2c\x30\x6c\x35\x2e\x39\x35\x32\ -\x32\x2c\x31\x34\x2e\x38\x32\x32\x32\x63\x30\x2e\x32\x38\x36\x31\ -\x2c\x30\x2e\x37\x31\x32\x34\x20\x30\x2e\x39\x35\x34\x37\x2c\x31\ -\x2e\x31\x39\x38\x31\x20\x31\x2e\x37\x32\x30\x37\x2c\x31\x2e\x32\ -\x35\x30\x31\x6c\x31\x35\x2e\x39\x33\x36\x2c\x31\x2e\x30\x38\x30\ -\x35\x63\x31\x2e\x37\x39\x38\x37\x2c\x30\x2e\x31\x32\x32\x20\x32\ -\x2e\x35\x33\x30\x35\x2c\x32\x2e\x33\x37\x34\x34\x20\x31\x2e\x31\ -\x34\x37\x2c\x33\x2e\x35\x33\x30\x32\x6c\x2d\x31\x32\x2e\x32\x35\ -\x37\x34\x2c\x31\x30\x2e\x32\x34\x31\x32\x63\x2d\x30\x2e\x35\x38\ -\x39\x31\x2c\x30\x2e\x34\x39\x32\x32\x20\x2d\x30\x2e\x38\x34\x34\ -\x35\x2c\x31\x2e\x32\x37\x38\x32\x20\x2d\x30\x2e\x36\x35\x37\x32\ -\x2c\x32\x2e\x30\x32\x32\x37\x6c\x33\x2e\x38\x39\x36\x39\x2c\x31\ -\x35\x2e\x34\x39\x63\x30\x2e\x34\x33\x39\x38\x2c\x31\x2e\x37\x34\ -\x38\x33\x20\x2d\x31\x2e\x34\x37\x36\x32\x2c\x33\x2e\x31\x34\x30\ -\x34\x20\x2d\x33\x2e\x30\x30\x33\x2c\x32\x2e\x31\x38\x31\x38\x6c\ -\x2d\x31\x33\x2e\x35\x32\x37\x37\x2c\x2d\x38\x2e\x34\x39\x32\x38\ -\x63\x2d\x30\x2e\x36\x35\x30\x32\x2c\x2d\x30\x2e\x34\x30\x38\x32\ -\x20\x2d\x31\x2e\x34\x37\x36\x36\x2c\x2d\x30\x2e\x34\x30\x38\x32\ -\x20\x2d\x32\x2e\x31\x32\x36\x38\x2c\x30\x6c\x2d\x31\x33\x2e\x35\ -\x32\x37\x37\x2c\x38\x2e\x34\x39\x32\x38\x63\x2d\x31\x2e\x35\x32\ -\x36\x38\x2c\x30\x2e\x39\x35\x38\x36\x20\x2d\x33\x2e\x34\x34\x32\ -\x38\x2c\x2d\x30\x2e\x34\x33\x33\x35\x20\x2d\x33\x2e\x30\x30\x33\ -\x2c\x2d\x32\x2e\x31\x38\x31\x38\x6c\x33\x2e\x38\x39\x36\x39\x2c\ -\x2d\x31\x35\x2e\x34\x39\x63\x30\x2e\x31\x38\x37\x33\x2c\x2d\x30\ -\x2e\x37\x34\x34\x35\x20\x2d\x30\x2e\x30\x36\x38\x31\x2c\x2d\x31\ -\x2e\x35\x33\x30\x35\x20\x2d\x30\x2e\x36\x35\x37\x32\x2c\x2d\x32\ -\x2e\x30\x32\x32\x37\x6c\x2d\x31\x32\x2e\x32\x35\x37\x34\x2c\x2d\ -\x31\x30\x2e\x32\x34\x31\x32\x63\x2d\x31\x2e\x33\x38\x33\x35\x2c\ -\x2d\x31\x2e\x31\x35\x35\x38\x20\x2d\x30\x2e\x36\x35\x31\x37\x2c\ -\x2d\x33\x2e\x34\x30\x38\x32\x20\x31\x2e\x31\x34\x37\x2c\x2d\x33\ -\x2e\x35\x33\x30\x32\x6c\x31\x35\x2e\x39\x33\x36\x2c\x2d\x31\x2e\ -\x30\x38\x30\x35\x63\x30\x2e\x37\x36\x36\x2c\x2d\x30\x2e\x30\x35\ -\x32\x20\x31\x2e\x34\x33\x34\x36\x2c\x2d\x30\x2e\x35\x33\x37\x37\ -\x20\x31\x2e\x37\x32\x30\x37\x2c\x2d\x31\x2e\x32\x35\x30\x31\x6c\ -\x35\x2e\x39\x35\x32\x32\x2c\x2d\x31\x34\x2e\x38\x32\x32\x32\x7a\ -\x22\x20\x66\x69\x6c\x6c\x3d\x22\x23\x46\x32\x39\x39\x34\x41\x22\ -\x20\x69\x64\x3d\x22\x73\x76\x67\x5f\x31\x22\x2f\x3e\x0a\x20\x20\ -\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x6d\x33\x39\x2e\x30\x35\x31\ -\x35\x2c\x32\x36\x2e\x33\x31\x30\x38\x63\x30\x2e\x33\x33\x35\x39\ -\x2c\x2d\x30\x2e\x38\x33\x36\x34\x20\x31\x2e\x35\x32\x30\x31\x2c\ -\x2d\x30\x2e\x38\x33\x36\x34\x20\x31\x2e\x38\x35\x36\x2c\x30\x6c\ -\x32\x2e\x39\x37\x36\x2c\x37\x2e\x34\x31\x31\x31\x63\x30\x2e\x31\ -\x34\x33\x31\x2c\x30\x2e\x33\x35\x36\x32\x20\x30\x2e\x34\x37\x37\ -\x34\x2c\x30\x2e\x35\x39\x39\x31\x20\x30\x2e\x38\x36\x30\x34\x2c\ -\x30\x2e\x36\x32\x35\x31\x6c\x37\x2e\x39\x36\x38\x2c\x30\x2e\x35\ -\x34\x30\x33\x63\x30\x2e\x38\x39\x39\x33\x2c\x30\x2e\x30\x36\x30\ -\x39\x20\x31\x2e\x32\x36\x35\x32\x2c\x31\x2e\x31\x38\x37\x31\x20\ -\x30\x2e\x35\x37\x33\x35\x2c\x31\x2e\x37\x36\x35\x31\x6c\x2d\x36\ -\x2e\x31\x32\x38\x37\x2c\x35\x2e\x31\x32\x30\x35\x63\x2d\x30\x2e\ -\x32\x39\x34\x36\x2c\x30\x2e\x32\x34\x36\x32\x20\x2d\x30\x2e\x34\ -\x32\x32\x33\x2c\x30\x2e\x36\x33\x39\x32\x20\x2d\x30\x2e\x33\x32\ -\x38\x36\x2c\x31\x2e\x30\x31\x31\x34\x6c\x31\x2e\x39\x34\x38\x34\ -\x2c\x37\x2e\x37\x34\x35\x63\x30\x2e\x32\x31\x39\x39\x2c\x30\x2e\ -\x38\x37\x34\x32\x20\x2d\x30\x2e\x37\x33\x38\x31\x2c\x31\x2e\x35\ -\x37\x30\x32\x20\x2d\x31\x2e\x35\x30\x31\x35\x2c\x31\x2e\x30\x39\ -\x30\x39\x6c\x2d\x36\x2e\x37\x36\x33\x38\x2c\x2d\x34\x2e\x32\x34\ -\x36\x34\x63\x2d\x30\x2e\x33\x32\x35\x31\x2c\x2d\x30\x2e\x32\x30\ -\x34\x31\x20\x2d\x30\x2e\x37\x33\x38\x33\x2c\x2d\x30\x2e\x32\x30\ -\x34\x31\x20\x2d\x31\x2e\x30\x36\x33\x34\x2c\x30\x6c\x2d\x36\x2e\ -\x37\x36\x33\x38\x2c\x34\x2e\x32\x34\x36\x34\x63\x2d\x30\x2e\x37\ -\x36\x33\x35\x2c\x30\x2e\x34\x37\x39\x33\x20\x2d\x31\x2e\x37\x32\ -\x31\x34\x2c\x2d\x30\x2e\x32\x31\x36\x37\x20\x2d\x31\x2e\x35\x30\ -\x31\x35\x2c\x2d\x31\x2e\x30\x39\x30\x39\x6c\x31\x2e\x39\x34\x38\ -\x34\x2c\x2d\x37\x2e\x37\x34\x35\x63\x30\x2e\x30\x39\x33\x36\x2c\ -\x2d\x30\x2e\x33\x37\x32\x32\x20\x2d\x30\x2e\x30\x33\x34\x31\x2c\ -\x2d\x30\x2e\x37\x36\x35\x32\x20\x2d\x30\x2e\x33\x32\x38\x36\x2c\ -\x2d\x31\x2e\x30\x31\x31\x34\x6c\x2d\x36\x2e\x31\x32\x38\x37\x2c\ -\x2d\x35\x2e\x31\x32\x30\x35\x63\x2d\x30\x2e\x36\x39\x31\x38\x2c\ -\x2d\x30\x2e\x35\x37\x38\x20\x2d\x30\x2e\x33\x32\x35\x38\x2c\x2d\ -\x31\x2e\x37\x30\x34\x32\x20\x30\x2e\x35\x37\x33\x35\x2c\x2d\x31\ -\x2e\x37\x36\x35\x31\x6c\x37\x2e\x39\x36\x38\x2c\x2d\x30\x2e\x35\ -\x34\x30\x33\x63\x30\x2e\x33\x38\x33\x2c\x2d\x30\x2e\x30\x32\x36\ -\x20\x30\x2e\x37\x31\x37\x33\x2c\x2d\x30\x2e\x32\x36\x38\x39\x20\ -\x30\x2e\x38\x36\x30\x33\x2c\x2d\x30\x2e\x36\x32\x35\x31\x6c\x32\ -\x2e\x39\x37\x36\x31\x2c\x2d\x37\x2e\x34\x31\x31\x31\x7a\x22\x20\ -\x66\x69\x6c\x6c\x3d\x22\x23\x46\x32\x43\x39\x34\x43\x22\x20\x69\ -\x64\x3d\x22\x73\x76\x67\x5f\x32\x22\x2f\x3e\x0a\x20\x20\x3c\x70\ -\x61\x74\x68\x20\x66\x69\x6c\x6c\x3d\x22\x23\x66\x66\x30\x30\x30\ -\x30\x22\x20\x64\x3d\x22\x6d\x35\x2c\x33\x39\x2e\x39\x39\x39\x39\ -\x34\x6c\x30\x2c\x30\x63\x30\x2c\x2d\x31\x38\x2e\x31\x36\x32\x36\ -\x38\x20\x31\x35\x2e\x36\x37\x30\x30\x35\x2c\x2d\x33\x32\x2e\x38\ -\x38\x36\x35\x34\x20\x33\x34\x2e\x39\x39\x39\x39\x37\x2c\x2d\x33\ -\x32\x2e\x38\x38\x36\x35\x34\x6c\x30\x2c\x30\x63\x39\x2e\x32\x38\ -\x32\x37\x2c\x30\x20\x31\x38\x2e\x31\x38\x35\x31\x37\x2c\x33\x2e\ -\x34\x36\x34\x38\x33\x20\x32\x34\x2e\x37\x34\x38\x37\x34\x2c\x39\ -\x2e\x36\x33\x32\x32\x38\x63\x36\x2e\x35\x36\x33\x38\x33\x2c\x36\ -\x2e\x31\x36\x37\x34\x35\x20\x31\x30\x2e\x32\x35\x31\x32\x39\x2c\ -\x31\x34\x2e\x35\x33\x32\x33\x32\x20\x31\x30\x2e\x32\x35\x31\x32\ -\x39\x2c\x32\x33\x2e\x32\x35\x34\x32\x36\x6c\x30\x2c\x30\x63\x30\ -\x2c\x31\x38\x2e\x31\x36\x32\x39\x20\x2d\x31\x35\x2e\x36\x36\x39\ -\x39\x34\x2c\x33\x32\x2e\x38\x38\x36\x36\x36\x20\x2d\x33\x35\x2e\ -\x30\x30\x30\x30\x33\x2c\x33\x32\x2e\x38\x38\x36\x36\x36\x6c\x30\ -\x2c\x30\x63\x2d\x31\x39\x2e\x33\x32\x39\x39\x32\x2c\x30\x20\x2d\ -\x33\x34\x2e\x39\x39\x39\x39\x37\x2c\x2d\x31\x34\x2e\x37\x32\x33\ -\x37\x36\x20\x2d\x33\x34\x2e\x39\x39\x39\x39\x37\x2c\x2d\x33\x32\ -\x2e\x38\x38\x36\x36\x36\x6c\x30\x2c\x30\x7a\x6d\x35\x36\x2e\x35\ -\x31\x37\x30\x33\x2c\x31\x34\x2e\x37\x31\x31\x38\x6c\x30\x2c\x30\ -\x63\x37\x2e\x37\x30\x35\x38\x34\x2c\x2d\x39\x2e\x39\x35\x30\x37\ -\x31\x20\x36\x2e\x35\x36\x30\x33\x39\x2c\x2d\x32\x33\x2e\x36\x39\ -\x30\x33\x36\x20\x2d\x32\x2e\x37\x30\x30\x35\x37\x2c\x2d\x33\x32\ -\x2e\x33\x39\x32\x63\x2d\x39\x2e\x32\x36\x30\x39\x36\x2c\x2d\x38\ -\x2e\x37\x30\x31\x37\x31\x20\x2d\x32\x33\x2e\x38\x38\x33\x35\x35\ -\x2c\x2d\x39\x2e\x37\x37\x38\x20\x2d\x33\x34\x2e\x34\x37\x33\x34\ -\x35\x2c\x2d\x32\x2e\x35\x33\x37\x33\x38\x6c\x33\x37\x2e\x31\x37\ -\x34\x30\x32\x2c\x33\x34\x2e\x39\x32\x39\x33\x38\x7a\x6d\x2d\x34\ -\x33\x2e\x30\x33\x33\x39\x33\x2c\x2d\x32\x39\x2e\x34\x32\x33\x33\ -\x63\x2d\x37\x2e\x37\x30\x35\x39\x2c\x39\x2e\x39\x35\x30\x36\x36\ -\x20\x2d\x36\x2e\x35\x36\x30\x34\x37\x2c\x32\x33\x2e\x36\x39\x30\ -\x33\x20\x32\x2e\x37\x30\x30\x34\x34\x2c\x33\x32\x2e\x33\x39\x31\ -\x38\x32\x63\x39\x2e\x32\x36\x30\x38\x38\x2c\x38\x2e\x37\x30\x31\ -\x37\x36\x20\x32\x33\x2e\x38\x38\x33\x34\x37\x2c\x39\x2e\x37\x37\ -\x38\x30\x35\x20\x33\x34\x2e\x34\x37\x33\x33\x37\x2c\x32\x2e\x35\ -\x33\x37\x35\x6c\x2d\x33\x37\x2e\x31\x37\x33\x38\x31\x2c\x2d\x33\ -\x34\x2e\x39\x32\x39\x33\x32\x6c\x30\x2c\x30\x7a\x22\x20\x69\x64\ -\x3d\x22\x73\x76\x67\x5f\x34\x22\x2f\x3e\x0a\x20\x3c\x2f\x67\x3e\ -\x0a\x0a\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x01\x6c\ -\x3c\ -\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ -\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ -\x30\x2f\x73\x76\x67\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x32\x34\ -\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x32\x34\x22\x20\x76\x69\ -\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\x30\x20\x32\x34\x20\x32\x34\ -\x22\x20\x66\x69\x6c\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\x20\x73\x74\ -\x72\x6f\x6b\x65\x3d\x22\x63\x75\x72\x72\x65\x6e\x74\x43\x6f\x6c\ -\x6f\x72\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\x68\ -\x3d\x22\x32\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\ -\x63\x61\x70\x3d\x22\x72\x6f\x75\x6e\x64\x22\x20\x73\x74\x72\x6f\ -\x6b\x65\x2d\x6c\x69\x6e\x65\x6a\x6f\x69\x6e\x3d\x22\x72\x6f\x75\ -\x6e\x64\x22\x20\x63\x6c\x61\x73\x73\x3d\x22\x66\x65\x61\x74\x68\ -\x65\x72\x20\x66\x65\x61\x74\x68\x65\x72\x2d\x73\x68\x61\x72\x65\ -\x22\x3e\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\x34\x20\x31\x32\ -\x76\x38\x61\x32\x20\x32\x20\x30\x20\x30\x20\x30\x20\x32\x20\x32\ -\x68\x31\x32\x61\x32\x20\x32\x20\x30\x20\x30\x20\x30\x20\x32\x2d\ -\x32\x76\x2d\x38\x22\x3e\x3c\x2f\x70\x61\x74\x68\x3e\x3c\x70\x6f\ -\x6c\x79\x6c\x69\x6e\x65\x20\x70\x6f\x69\x6e\x74\x73\x3d\x22\x31\ -\x36\x20\x36\x20\x31\x32\x20\x32\x20\x38\x20\x36\x22\x3e\x3c\x2f\ -\x70\x6f\x6c\x79\x6c\x69\x6e\x65\x3e\x3c\x6c\x69\x6e\x65\x20\x78\ -\x31\x3d\x22\x31\x32\x22\x20\x79\x31\x3d\x22\x32\x22\x20\x78\x32\ -\x3d\x22\x31\x32\x22\x20\x79\x32\x3d\x22\x31\x35\x22\x3e\x3c\x2f\ -\x6c\x69\x6e\x65\x3e\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x01\x33\ -\x3c\ -\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ -\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ -\x30\x2f\x73\x76\x67\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x32\x34\ -\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x32\x34\x22\x20\x76\x69\ -\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\x30\x20\x32\x34\x20\x32\x34\ -\x22\x20\x66\x69\x6c\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\x20\x73\x74\ -\x72\x6f\x6b\x65\x3d\x22\x63\x75\x72\x72\x65\x6e\x74\x43\x6f\x6c\ -\x6f\x72\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\x68\ -\x3d\x22\x32\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\ -\x63\x61\x70\x3d\x22\x72\x6f\x75\x6e\x64\x22\x20\x73\x74\x72\x6f\ -\x6b\x65\x2d\x6c\x69\x6e\x65\x6a\x6f\x69\x6e\x3d\x22\x72\x6f\x75\ -\x6e\x64\x22\x20\x63\x6c\x61\x73\x73\x3d\x22\x66\x65\x61\x74\x68\ -\x65\x72\x20\x66\x65\x61\x74\x68\x65\x72\x2d\x63\x6f\x64\x65\x22\ -\x3e\x3c\x70\x6f\x6c\x79\x6c\x69\x6e\x65\x20\x70\x6f\x69\x6e\x74\ -\x73\x3d\x22\x31\x36\x20\x31\x38\x20\x32\x32\x20\x31\x32\x20\x31\ -\x36\x20\x36\x22\x3e\x3c\x2f\x70\x6f\x6c\x79\x6c\x69\x6e\x65\x3e\ -\x3c\x70\x6f\x6c\x79\x6c\x69\x6e\x65\x20\x70\x6f\x69\x6e\x74\x73\ -\x3d\x22\x38\x20\x36\x20\x32\x20\x31\x32\x20\x38\x20\x31\x38\x22\ -\x3e\x3c\x2f\x70\x6f\x6c\x79\x6c\x69\x6e\x65\x3e\x3c\x2f\x73\x76\ -\x67\x3e\ -\x00\x00\x03\xa8\ -\x00\ -\x00\x0c\x99\x78\x9c\xdd\x56\x4b\x6f\xe3\x36\x10\xbe\xe7\x57\x08\ -\xca\xa5\x45\x23\x8a\xd4\xcb\xa2\x62\x79\x81\x36\x58\xb4\x87\x5e\ -\xba\xbb\xe8\x99\x21\x69\x5b\x1b\x89\x34\x28\x3a\xb6\xf7\xd7\xef\ -\x50\x0f\x5b\x76\x9c\xf4\x71\x28\xd0\x08\x36\xec\x79\x71\x66\xbe\ -\xf9\x38\xf6\xfc\xc3\xbe\xa9\xbd\x67\x69\xda\x4a\xab\xd2\x27\x08\ -\xfb\x9e\x54\x5c\x8b\x4a\xad\x4a\xff\xcb\xe7\x8f\x41\xee\x7b\xad\ -\x65\x4a\xb0\x5a\x2b\x59\xfa\x4a\xfb\x1f\x16\x37\xf3\xf6\x79\x75\ -\xe3\x79\x1e\x04\xab\xb6\x10\xbc\xf4\xd7\xd6\x6e\x8a\x30\xdc\x6c\ -\x4d\x8d\xb4\x59\x85\x82\x87\xb2\x96\x8d\x54\xb6\x0d\x09\x22\xa1\ -\x7f\x72\xe7\x27\x77\x6e\x24\xb3\xd5\xb3\xe4\xba\x69\xb4\x6a\xbb\ -\x48\xd5\xde\x4e\x9c\x8d\x58\x1e\xbd\x77\xbb\x1d\xda\xc5\x9d\x13\ -\xa1\x94\x86\x38\x0a\xa3\x28\x00\x8f\xa0\x3d\x28\xcb\xf6\xc1\x79\ -\x28\xd4\x78\x2d\x34\xc2\x18\x87\x60\x3b\x79\xfe\x3d\xaf\xa2\x05\ -\x54\x36\xf0\x3e\xba\x8f\x0a\xd4\xea\xad\xe1\x72\x09\x71\x12\x29\ -\x69\xc3\x87\xcf\x0f\x47\x63\x80\x91\xb0\x62\x72\x4c\xa5\x9e\x5a\ -\xce\x36\xf2\x2c\xeb\xa8\xec\x11\x60\x8d\x6c\x37\x8c\xcb\x36\x1c\ -\xf5\x5d\xfc\x28\x14\xd3\x79\x19\x4e\xbc\x1f\x30\xa5\x19\x16\xd9\ -\x12\xa7\x77\x5e\x84\x23\x1c\xe0\x24\xc0\xf4\xc7\x2e\x6a\x2c\xa4\ -\x10\x9a\xbb\x93\x4b\x5f\xee\x37\x30\x50\x34\x76\x57\x89\xd2\x87\ -\xef\x59\x27\x4c\x8e\x26\x9d\x82\xd7\xac\x05\x84\x96\x30\xa8\xb5\ -\x34\xde\xf0\x19\x00\x45\xfa\xa2\x5a\x6b\xf4\x93\x0c\xea\x4a\xc9\ -\xaf\xba\x82\x40\xa3\xb7\x4a\x5c\x9a\xa0\xec\x2b\x96\x5d\x25\xec\ -\xba\xf4\xa3\x89\xae\xf4\xf9\xd6\x18\xa0\xcd\x2f\xba\xd6\xa6\x33\ -\x2c\xab\xba\x76\xc4\x53\x7d\xc2\xe7\x4a\xee\x7e\xd6\xfb\xd2\xc7\ -\x1e\xf6\xa2\x04\x5e\x9d\x7a\x2d\xab\xd5\xda\xc2\x61\xbd\x38\x1e\ -\x9d\xf8\x0b\x10\xe7\x8d\xb4\x4c\x30\xcb\x9c\xa9\xef\x78\xd4\x90\ -\xa8\xf3\x00\x1f\x20\x52\xf1\xc7\xc3\xc7\x5e\x02\x99\xf3\xe2\x4f\ -\x6d\x9e\x06\x11\x1e\xe7\xc0\x1e\xf5\x16\xb2\xf8\x8b\xa3\x7a\x2e\ -\x78\x01\xa3\x6f\x98\x5d\x54\x0d\x5b\x49\xc7\x9a\x9f\x60\xd4\xf3\ -\xf0\x64\x38\x73\xb6\x87\x8d\x3c\x1d\xda\x1f\x6b\x64\xcf\xa1\xab\ -\x17\x49\xf0\xa6\x72\x41\xe1\x27\x0b\x50\xfc\xe6\x92\xf8\x5e\x78\ -\x71\x68\x65\x6b\xb9\xe8\x72\xf6\x5f\xc7\x2e\xc2\xa1\x8d\xa1\xc9\ -\x70\xd2\xe5\x3c\x1c\x41\xe8\x24\x21\x97\xed\x09\x1f\x27\x11\x3c\ -\xe4\x99\x1f\x49\xe4\x18\x24\xdc\x08\x06\xcf\x91\x92\xc3\xd4\x82\ -\x9a\x1d\xa4\x99\xf0\x69\xe2\xb2\xab\x94\xd0\xbb\xa0\x61\xfb\xaa\ -\xa9\xbe\x49\xc8\x81\x5f\x71\x39\x00\xfd\xf2\xf4\x15\x23\x4c\x9e\ -\xc4\xf9\xec\xd2\xca\x5d\x50\x84\x32\x1c\xc7\xf1\x8b\xd4\x7c\xdf\ -\x19\x93\x59\x7c\x25\xf2\x9b\xd6\x0d\x30\x85\xa2\x8c\xe6\xc9\x31\ -\x6d\xbb\xd6\xbb\x95\x71\x48\x2c\x59\xdd\x4a\xff\x84\xcc\x11\x82\ -\xfc\x95\x0a\x47\x2a\x12\x12\xbd\xe6\x32\xd0\x93\xd0\x59\x72\xe9\ -\xb1\x81\xf1\xb6\x6b\x06\x5e\xe3\xcd\xb8\x30\x6a\x58\x0d\xc0\x87\ -\x13\x7c\xab\x6d\x25\xa4\xd5\xb5\x34\x4c\x39\x0a\x91\xa3\x01\xea\ -\xbf\xa6\xd7\x8f\x5f\x25\xb7\xd7\x2c\x8f\xda\x08\x69\x8e\x19\xc8\ -\x99\x9a\xbb\x2b\x59\xfa\xb7\x59\xf7\x0c\x26\x57\xd1\x68\x58\x76\ -\xcf\xc8\x99\x0d\x6c\x8a\x01\x4b\x7b\xa8\x21\x8b\xbb\xc8\x85\xbb\ -\xc7\xf7\xfd\x5d\x2f\x6e\x71\xf7\xdc\x4f\xd7\x41\x11\xdd\x9f\xef\ -\x8d\xa2\x5b\x1b\xf7\x17\x7b\xa6\x80\x2b\x21\xcd\xa8\xed\x84\x1a\ -\x68\x65\x8b\x64\xd4\x09\x06\x28\x1a\xc3\x0e\xd3\x94\xc1\xd0\x5a\ -\x31\x76\x06\xf3\xfc\xdd\x4b\x50\x84\x73\x9a\xe5\xf4\x2e\x42\x69\ -\x4a\x71\x1a\x13\xef\x57\x2f\x22\x28\x4d\x28\x8d\xc8\x64\xf6\xae\ -\xa7\x3c\x7d\x49\x3e\xad\xa0\x56\xab\x61\x2f\x6e\xcd\x33\xb3\x5b\ -\x23\xdd\x78\xfe\xcf\x40\x10\x44\x31\xc5\x79\xf6\x26\x10\xf4\xfd\ -\x03\x41\x30\x8a\x28\xc5\xd9\xec\x2d\x20\x72\xf2\x5e\x81\x98\xa1\ -\x2c\x89\x93\x7c\x96\xdd\x65\x28\x89\x00\x87\x37\x61\x78\xb1\xb3\ -\xdf\x1f\x0c\x24\x41\x24\xa3\x78\x16\xbf\x09\xc4\xbf\xde\x10\x7f\ -\x15\x70\x79\x03\xd3\x53\x95\x8d\x07\x3f\x71\x18\xb8\x98\x02\x6b\ -\x73\x84\x63\xa8\x92\x7a\x6b\x8f\xa2\x24\x23\xb3\x6c\xfc\x2d\xf9\ -\x47\x50\xc3\x16\x48\xd3\x28\xff\x8f\x00\x77\x68\xcc\xdd\xff\xa7\ -\xc5\xcd\x77\xd2\xf3\xe7\xb2\ -\x00\x00\x03\x9b\ -\x3c\ -\x73\x76\x67\x20\x77\x69\x64\x74\x68\x3d\x22\x38\x30\x22\x20\x68\ -\x65\x69\x67\x68\x74\x3d\x22\x38\x30\x22\x20\x76\x69\x65\x77\x42\ -\x6f\x78\x3d\x22\x30\x20\x30\x20\x38\x30\x20\x38\x30\x22\x20\x66\ -\x69\x6c\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\x20\x78\x6d\x6c\x6e\x73\ -\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\ -\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x3e\x0a\x20\ -\x20\x3c\x70\x61\x74\x68\x20\x66\x69\x6c\x6c\x2d\x72\x75\x6c\x65\ -\x3d\x22\x65\x76\x65\x6e\x6f\x64\x64\x22\x20\x63\x6c\x69\x70\x2d\ -\x72\x75\x6c\x65\x3d\x22\x65\x76\x65\x6e\x6f\x64\x64\x22\x20\x64\ -\x3d\x22\x4d\x33\x33\x2e\x39\x32\x35\x36\x20\x39\x2e\x38\x38\x36\ -\x34\x37\x43\x33\x33\x2e\x31\x36\x34\x38\x20\x39\x2e\x38\x38\x36\ -\x34\x37\x20\x33\x32\x2e\x34\x34\x35\x34\x20\x31\x30\x2e\x32\x33\ -\x32\x39\x20\x33\x31\x2e\x39\x37\x31\x31\x20\x31\x30\x2e\x38\x32\ -\x37\x37\x4c\x32\x36\x2e\x36\x35\x30\x31\x20\x31\x37\x2e\x35\x48\ -\x31\x35\x43\x31\x33\x2e\x36\x31\x39\x33\x20\x31\x37\x2e\x35\x20\ -\x31\x32\x2e\x35\x20\x31\x38\x2e\x36\x31\x39\x33\x20\x31\x32\x2e\ -\x35\x20\x32\x30\x43\x31\x32\x2e\x35\x20\x32\x31\x2e\x33\x38\x30\ -\x37\x20\x31\x33\x2e\x36\x31\x39\x33\x20\x32\x32\x2e\x35\x20\x31\ -\x35\x20\x32\x32\x2e\x35\x48\x31\x36\x2e\x35\x56\x36\x34\x43\x31\ -\x36\x2e\x35\x20\x36\x37\x2e\x35\x38\x39\x39\x20\x31\x39\x2e\x34\ -\x31\x30\x31\x20\x37\x30\x2e\x35\x20\x32\x33\x20\x37\x30\x2e\x35\ -\x48\x35\x37\x43\x36\x30\x2e\x35\x38\x39\x38\x20\x37\x30\x2e\x35\ -\x20\x36\x33\x2e\x35\x20\x36\x37\x2e\x35\x38\x39\x39\x20\x36\x33\ -\x2e\x35\x20\x36\x34\x56\x32\x32\x2e\x35\x48\x36\x35\x43\x36\x36\ -\x2e\x33\x38\x30\x37\x20\x32\x32\x2e\x35\x20\x36\x37\x2e\x35\x20\ -\x32\x31\x2e\x33\x38\x30\x37\x20\x36\x37\x2e\x35\x20\x32\x30\x43\ -\x36\x37\x2e\x35\x20\x31\x38\x2e\x36\x31\x39\x33\x20\x36\x36\x2e\ -\x33\x38\x30\x37\x20\x31\x37\x2e\x35\x20\x36\x35\x20\x31\x37\x2e\ -\x35\x48\x35\x33\x2e\x33\x34\x39\x39\x4c\x34\x38\x2e\x30\x32\x39\ -\x20\x31\x30\x2e\x38\x32\x37\x38\x43\x34\x37\x2e\x35\x35\x34\x36\ -\x20\x31\x30\x2e\x32\x33\x32\x39\x20\x34\x36\x2e\x38\x33\x35\x32\ -\x20\x39\x2e\x38\x38\x36\x34\x37\x20\x34\x36\x2e\x30\x37\x34\x34\ -\x20\x39\x2e\x38\x38\x36\x34\x37\x48\x33\x33\x2e\x39\x32\x35\x36\ -\x5a\x4d\x33\x33\x20\x32\x37\x2e\x35\x43\x33\x34\x2e\x33\x38\x30\ -\x37\x20\x32\x37\x2e\x35\x20\x33\x35\x2e\x35\x20\x32\x38\x2e\x36\ -\x31\x39\x33\x20\x33\x35\x2e\x35\x20\x33\x30\x56\x35\x38\x43\x33\ -\x35\x2e\x35\x20\x35\x39\x2e\x33\x38\x30\x37\x20\x33\x34\x2e\x33\ -\x38\x30\x37\x20\x36\x30\x2e\x35\x20\x33\x33\x20\x36\x30\x2e\x35\ -\x43\x33\x31\x2e\x36\x31\x39\x33\x20\x36\x30\x2e\x35\x20\x33\x30\ -\x2e\x35\x20\x35\x39\x2e\x33\x38\x30\x37\x20\x33\x30\x2e\x35\x20\ -\x35\x38\x56\x33\x30\x43\x33\x30\x2e\x35\x20\x32\x38\x2e\x36\x31\ -\x39\x33\x20\x33\x31\x2e\x36\x31\x39\x33\x20\x32\x37\x2e\x35\x20\ -\x33\x33\x20\x32\x37\x2e\x35\x5a\x4d\x34\x39\x2e\x35\x20\x33\x30\ -\x43\x34\x39\x2e\x35\x20\x32\x38\x2e\x36\x31\x39\x33\x20\x34\x38\ -\x2e\x33\x38\x30\x37\x20\x32\x37\x2e\x35\x20\x34\x37\x20\x32\x37\ -\x2e\x35\x43\x34\x35\x2e\x36\x31\x39\x33\x20\x32\x37\x2e\x35\x20\ -\x34\x34\x2e\x35\x20\x32\x38\x2e\x36\x31\x39\x33\x20\x34\x34\x2e\ -\x35\x20\x33\x30\x56\x35\x38\x43\x34\x34\x2e\x35\x20\x35\x39\x2e\ -\x33\x38\x30\x37\x20\x34\x35\x2e\x36\x31\x39\x33\x20\x36\x30\x2e\ -\x35\x20\x34\x37\x20\x36\x30\x2e\x35\x43\x34\x38\x2e\x33\x38\x30\ -\x37\x20\x36\x30\x2e\x35\x20\x34\x39\x2e\x35\x20\x35\x39\x2e\x33\ -\x38\x30\x37\x20\x34\x39\x2e\x35\x20\x35\x38\x56\x33\x30\x5a\x4d\ -\x34\x36\x2e\x39\x35\x33\x36\x20\x31\x37\x2e\x34\x39\x38\x36\x4c\ -\x34\x34\x2e\x38\x37\x30\x34\x20\x31\x34\x2e\x38\x38\x36\x35\x48\ -\x33\x35\x2e\x31\x32\x39\x36\x4c\x33\x33\x2e\x30\x34\x36\x34\x20\ -\x31\x37\x2e\x34\x39\x38\x36\x48\x34\x36\x2e\x39\x35\x33\x36\x5a\ -\x22\x20\x66\x69\x6c\x6c\x3d\x22\x23\x43\x32\x43\x43\x44\x45\x22\ -\x20\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x01\x88\ -\x3c\ -\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ -\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ -\x30\x2f\x73\x76\x67\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x32\x34\ -\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x32\x34\x22\x20\x76\x69\ -\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\x30\x20\x32\x34\x20\x32\x34\ -\x22\x20\x66\x69\x6c\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\x20\x73\x74\ -\x72\x6f\x6b\x65\x3d\x22\x63\x75\x72\x72\x65\x6e\x74\x43\x6f\x6c\ -\x6f\x72\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\x68\ -\x3d\x22\x32\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\ -\x63\x61\x70\x3d\x22\x72\x6f\x75\x6e\x64\x22\x20\x73\x74\x72\x6f\ -\x6b\x65\x2d\x6c\x69\x6e\x65\x6a\x6f\x69\x6e\x3d\x22\x72\x6f\x75\ -\x6e\x64\x22\x20\x63\x6c\x61\x73\x73\x3d\x22\x66\x65\x61\x74\x68\ -\x65\x72\x20\x66\x65\x61\x74\x68\x65\x72\x2d\x73\x61\x76\x65\x22\ -\x3e\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\x31\x39\x20\x32\x31\ -\x48\x35\x61\x32\x20\x32\x20\x30\x20\x30\x20\x31\x2d\x32\x2d\x32\ -\x56\x35\x61\x32\x20\x32\x20\x30\x20\x30\x20\x31\x20\x32\x2d\x32\ -\x68\x31\x31\x6c\x35\x20\x35\x76\x31\x31\x61\x32\x20\x32\x20\x30\ -\x20\x30\x20\x31\x2d\x32\x20\x32\x7a\x22\x3e\x3c\x2f\x70\x61\x74\ -\x68\x3e\x3c\x70\x6f\x6c\x79\x6c\x69\x6e\x65\x20\x70\x6f\x69\x6e\ -\x74\x73\x3d\x22\x31\x37\x20\x32\x31\x20\x31\x37\x20\x31\x33\x20\ -\x37\x20\x31\x33\x20\x37\x20\x32\x31\x22\x3e\x3c\x2f\x70\x6f\x6c\ -\x79\x6c\x69\x6e\x65\x3e\x3c\x70\x6f\x6c\x79\x6c\x69\x6e\x65\x20\ -\x70\x6f\x69\x6e\x74\x73\x3d\x22\x37\x20\x33\x20\x37\x20\x38\x20\ -\x31\x35\x20\x38\x22\x3e\x3c\x2f\x70\x6f\x6c\x79\x6c\x69\x6e\x65\ -\x3e\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x01\x70\ -\x3c\ -\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ -\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ -\x30\x2f\x73\x76\x67\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x32\x34\ -\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x32\x34\x22\x20\x76\x69\ -\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\x30\x20\x32\x34\x20\x32\x34\ -\x22\x20\x66\x69\x6c\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\x20\x73\x74\ -\x72\x6f\x6b\x65\x3d\x22\x63\x75\x72\x72\x65\x6e\x74\x43\x6f\x6c\ -\x6f\x72\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\x68\ -\x3d\x22\x32\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\ -\x63\x61\x70\x3d\x22\x72\x6f\x75\x6e\x64\x22\x20\x73\x74\x72\x6f\ -\x6b\x65\x2d\x6c\x69\x6e\x65\x6a\x6f\x69\x6e\x3d\x22\x72\x6f\x75\ -\x6e\x64\x22\x20\x63\x6c\x61\x73\x73\x3d\x22\x66\x65\x61\x74\x68\ -\x65\x72\x20\x66\x65\x61\x74\x68\x65\x72\x2d\x6c\x6f\x67\x2d\x69\ -\x6e\x22\x3e\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\x31\x35\x20\ -\x33\x68\x34\x61\x32\x20\x32\x20\x30\x20\x30\x20\x31\x20\x32\x20\ -\x32\x76\x31\x34\x61\x32\x20\x32\x20\x30\x20\x30\x20\x31\x2d\x32\ -\x20\x32\x68\x2d\x34\x22\x3e\x3c\x2f\x70\x61\x74\x68\x3e\x3c\x70\ -\x6f\x6c\x79\x6c\x69\x6e\x65\x20\x70\x6f\x69\x6e\x74\x73\x3d\x22\ -\x31\x30\x20\x31\x37\x20\x31\x35\x20\x31\x32\x20\x31\x30\x20\x37\ -\x22\x3e\x3c\x2f\x70\x6f\x6c\x79\x6c\x69\x6e\x65\x3e\x3c\x6c\x69\ -\x6e\x65\x20\x78\x31\x3d\x22\x31\x35\x22\x20\x79\x31\x3d\x22\x31\ -\x32\x22\x20\x78\x32\x3d\x22\x33\x22\x20\x79\x32\x3d\x22\x31\x32\ -\x22\x3e\x3c\x2f\x6c\x69\x6e\x65\x3e\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x05\xba\ -\x3c\ -\x73\x76\x67\x20\x77\x69\x64\x74\x68\x3d\x22\x38\x30\x22\x20\x68\ -\x65\x69\x67\x68\x74\x3d\x22\x38\x30\x22\x20\x76\x69\x65\x77\x42\ -\x6f\x78\x3d\x22\x30\x20\x30\x20\x38\x30\x20\x38\x30\x22\x20\x66\ -\x69\x6c\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\x20\x78\x6d\x6c\x6e\x73\ -\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\ -\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x3e\x0a\x20\ -\x20\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\x33\x38\x2e\x31\x34\ -\x34\x31\x20\x31\x32\x2e\x36\x32\x31\x37\x43\x33\x38\x2e\x38\x31\ -\x35\x39\x20\x31\x30\x2e\x39\x34\x38\x38\x20\x34\x31\x2e\x31\x38\ -\x34\x31\x20\x31\x30\x2e\x39\x34\x38\x38\x20\x34\x31\x2e\x38\x35\ -\x35\x39\x20\x31\x32\x2e\x36\x32\x31\x37\x4c\x34\x37\x2e\x38\x30\ -\x38\x31\x20\x32\x37\x2e\x34\x34\x33\x39\x43\x34\x38\x2e\x30\x39\ -\x34\x32\x20\x32\x38\x2e\x31\x35\x36\x33\x20\x34\x38\x2e\x37\x36\ -\x32\x38\x20\x32\x38\x2e\x36\x34\x32\x20\x34\x39\x2e\x35\x32\x38\ -\x38\x20\x32\x38\x2e\x36\x39\x34\x4c\x36\x35\x2e\x34\x36\x34\x38\ -\x20\x32\x39\x2e\x37\x37\x34\x35\x43\x36\x37\x2e\x32\x36\x33\x35\ -\x20\x32\x39\x2e\x38\x39\x36\x35\x20\x36\x37\x2e\x39\x39\x35\x33\ -\x20\x33\x32\x2e\x31\x34\x38\x39\x20\x36\x36\x2e\x36\x31\x31\x38\ -\x20\x33\x33\x2e\x33\x30\x34\x37\x4c\x35\x34\x2e\x33\x35\x34\x34\ -\x20\x34\x33\x2e\x35\x34\x35\x39\x43\x35\x33\x2e\x37\x36\x35\x33\ -\x20\x34\x34\x2e\x30\x33\x38\x31\x20\x35\x33\x2e\x35\x30\x39\x39\ -\x20\x34\x34\x2e\x38\x32\x34\x31\x20\x35\x33\x2e\x36\x39\x37\x32\ -\x20\x34\x35\x2e\x35\x36\x38\x36\x4c\x35\x37\x2e\x35\x39\x34\x31\ -\x20\x36\x31\x2e\x30\x35\x38\x36\x43\x35\x38\x2e\x30\x33\x33\x39\ -\x20\x36\x32\x2e\x38\x30\x36\x39\x20\x35\x36\x2e\x31\x31\x37\x39\ -\x20\x36\x34\x2e\x31\x39\x39\x20\x35\x34\x2e\x35\x39\x31\x31\x20\ -\x36\x33\x2e\x32\x34\x30\x34\x4c\x34\x31\x2e\x30\x36\x33\x34\x20\ -\x35\x34\x2e\x37\x34\x37\x36\x43\x34\x30\x2e\x34\x31\x33\x32\x20\ -\x35\x34\x2e\x33\x33\x39\x34\x20\x33\x39\x2e\x35\x38\x36\x38\x20\ -\x35\x34\x2e\x33\x33\x39\x34\x20\x33\x38\x2e\x39\x33\x36\x36\x20\ -\x35\x34\x2e\x37\x34\x37\x36\x4c\x32\x35\x2e\x34\x30\x38\x39\x20\ -\x36\x33\x2e\x32\x34\x30\x34\x43\x32\x33\x2e\x38\x38\x32\x31\x20\ -\x36\x34\x2e\x31\x39\x39\x20\x32\x31\x2e\x39\x36\x36\x31\x20\x36\ -\x32\x2e\x38\x30\x36\x39\x20\x32\x32\x2e\x34\x30\x35\x39\x20\x36\ -\x31\x2e\x30\x35\x38\x36\x4c\x32\x36\x2e\x33\x30\x32\x38\x20\x34\ -\x35\x2e\x35\x36\x38\x36\x43\x32\x36\x2e\x34\x39\x30\x31\x20\x34\ -\x34\x2e\x38\x32\x34\x31\x20\x32\x36\x2e\x32\x33\x34\x37\x20\x34\ -\x34\x2e\x30\x33\x38\x31\x20\x32\x35\x2e\x36\x34\x35\x36\x20\x34\ -\x33\x2e\x35\x34\x35\x39\x4c\x31\x33\x2e\x33\x38\x38\x32\x20\x33\ -\x33\x2e\x33\x30\x34\x37\x43\x31\x32\x2e\x30\x30\x34\x37\x20\x33\ -\x32\x2e\x31\x34\x38\x39\x20\x31\x32\x2e\x37\x33\x36\x35\x20\x32\ -\x39\x2e\x38\x39\x36\x35\x20\x31\x34\x2e\x35\x33\x35\x32\x20\x32\ -\x39\x2e\x37\x37\x34\x35\x4c\x33\x30\x2e\x34\x37\x31\x32\x20\x32\ -\x38\x2e\x36\x39\x34\x43\x33\x31\x2e\x32\x33\x37\x32\x20\x32\x38\ -\x2e\x36\x34\x32\x20\x33\x31\x2e\x39\x30\x35\x38\x20\x32\x38\x2e\ -\x31\x35\x36\x33\x20\x33\x32\x2e\x31\x39\x31\x39\x20\x32\x37\x2e\ -\x34\x34\x33\x39\x4c\x33\x38\x2e\x31\x34\x34\x31\x20\x31\x32\x2e\ -\x36\x32\x31\x37\x5a\x22\x20\x66\x69\x6c\x6c\x3d\x22\x23\x46\x32\ -\x39\x39\x34\x41\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x70\x61\x74\x68\ -\x20\x64\x3d\x22\x4d\x33\x39\x2e\x30\x35\x31\x35\x20\x32\x36\x2e\ -\x33\x31\x30\x38\x43\x33\x39\x2e\x33\x38\x37\x34\x20\x32\x35\x2e\ -\x34\x37\x34\x34\x20\x34\x30\x2e\x35\x37\x31\x36\x20\x32\x35\x2e\ -\x34\x37\x34\x34\x20\x34\x30\x2e\x39\x30\x37\x35\x20\x32\x36\x2e\ -\x33\x31\x30\x38\x4c\x34\x33\x2e\x38\x38\x33\x35\x20\x33\x33\x2e\ -\x37\x32\x31\x39\x43\x34\x34\x2e\x30\x32\x36\x36\x20\x33\x34\x2e\ -\x30\x37\x38\x31\x20\x34\x34\x2e\x33\x36\x30\x39\x20\x33\x34\x2e\ -\x33\x32\x31\x20\x34\x34\x2e\x37\x34\x33\x39\x20\x33\x34\x2e\x33\ -\x34\x37\x4c\x35\x32\x2e\x37\x31\x31\x39\x20\x33\x34\x2e\x38\x38\ -\x37\x33\x43\x35\x33\x2e\x36\x31\x31\x32\x20\x33\x34\x2e\x39\x34\ -\x38\x32\x20\x35\x33\x2e\x39\x37\x37\x31\x20\x33\x36\x2e\x30\x37\ -\x34\x34\x20\x35\x33\x2e\x32\x38\x35\x34\x20\x33\x36\x2e\x36\x35\ -\x32\x34\x4c\x34\x37\x2e\x31\x35\x36\x37\x20\x34\x31\x2e\x37\x37\ -\x32\x39\x43\x34\x36\x2e\x38\x36\x32\x31\x20\x34\x32\x2e\x30\x31\ -\x39\x31\x20\x34\x36\x2e\x37\x33\x34\x34\x20\x34\x32\x2e\x34\x31\ -\x32\x31\x20\x34\x36\x2e\x38\x32\x38\x31\x20\x34\x32\x2e\x37\x38\ -\x34\x33\x4c\x34\x38\x2e\x37\x37\x36\x35\x20\x35\x30\x2e\x35\x32\ -\x39\x33\x43\x34\x38\x2e\x39\x39\x36\x34\x20\x35\x31\x2e\x34\x30\ -\x33\x35\x20\x34\x38\x2e\x30\x33\x38\x34\x20\x35\x32\x2e\x30\x39\ -\x39\x35\x20\x34\x37\x2e\x32\x37\x35\x20\x35\x31\x2e\x36\x32\x30\ -\x32\x4c\x34\x30\x2e\x35\x31\x31\x32\x20\x34\x37\x2e\x33\x37\x33\ -\x38\x43\x34\x30\x2e\x31\x38\x36\x31\x20\x34\x37\x2e\x31\x36\x39\ -\x37\x20\x33\x39\x2e\x37\x37\x32\x39\x20\x34\x37\x2e\x31\x36\x39\ -\x37\x20\x33\x39\x2e\x34\x34\x37\x38\x20\x34\x37\x2e\x33\x37\x33\ -\x38\x4c\x33\x32\x2e\x36\x38\x34\x20\x35\x31\x2e\x36\x32\x30\x32\ -\x43\x33\x31\x2e\x39\x32\x30\x35\x20\x35\x32\x2e\x30\x39\x39\x35\ -\x20\x33\x30\x2e\x39\x36\x32\x36\x20\x35\x31\x2e\x34\x30\x33\x35\ -\x20\x33\x31\x2e\x31\x38\x32\x35\x20\x35\x30\x2e\x35\x32\x39\x33\ -\x4c\x33\x33\x2e\x31\x33\x30\x39\x20\x34\x32\x2e\x37\x38\x34\x33\ -\x43\x33\x33\x2e\x32\x32\x34\x35\x20\x34\x32\x2e\x34\x31\x32\x31\ -\x20\x33\x33\x2e\x30\x39\x36\x38\x20\x34\x32\x2e\x30\x31\x39\x31\ -\x20\x33\x32\x2e\x38\x30\x32\x33\x20\x34\x31\x2e\x37\x37\x32\x39\ -\x4c\x32\x36\x2e\x36\x37\x33\x36\x20\x33\x36\x2e\x36\x35\x32\x34\ -\x43\x32\x35\x2e\x39\x38\x31\x38\x20\x33\x36\x2e\x30\x37\x34\x34\ -\x20\x32\x36\x2e\x33\x34\x37\x38\x20\x33\x34\x2e\x39\x34\x38\x32\ -\x20\x32\x37\x2e\x32\x34\x37\x31\x20\x33\x34\x2e\x38\x38\x37\x33\ -\x4c\x33\x35\x2e\x32\x31\x35\x31\x20\x33\x34\x2e\x33\x34\x37\x43\ -\x33\x35\x2e\x35\x39\x38\x31\x20\x33\x34\x2e\x33\x32\x31\x20\x33\ -\x35\x2e\x39\x33\x32\x34\x20\x33\x34\x2e\x30\x37\x38\x31\x20\x33\ -\x36\x2e\x30\x37\x35\x34\x20\x33\x33\x2e\x37\x32\x31\x39\x4c\x33\ -\x39\x2e\x30\x35\x31\x35\x20\x32\x36\x2e\x33\x31\x30\x38\x5a\x22\ -\x20\x66\x69\x6c\x6c\x3d\x22\x23\x46\x32\x43\x39\x34\x43\x22\x20\ -\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x04\xc2\ -\x3c\ -\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ -\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x75\x74\x66\ -\x2d\x38\x22\x3f\x3e\x3c\x21\x2d\x2d\x20\x55\x70\x6c\x6f\x61\x64\ -\x65\x64\x20\x74\x6f\x3a\x20\x53\x56\x47\x20\x52\x65\x70\x6f\x2c\ -\x20\x77\x77\x77\x2e\x73\x76\x67\x72\x65\x70\x6f\x2e\x63\x6f\x6d\ -\x2c\x20\x47\x65\x6e\x65\x72\x61\x74\x6f\x72\x3a\x20\x53\x56\x47\ -\x20\x52\x65\x70\x6f\x20\x4d\x69\x78\x65\x72\x20\x54\x6f\x6f\x6c\ -\x73\x20\x2d\x2d\x3e\x0a\x3c\x73\x76\x67\x20\x77\x69\x64\x74\x68\ -\x3d\x22\x38\x30\x30\x70\x78\x22\x20\x68\x65\x69\x67\x68\x74\x3d\ -\x22\x38\x30\x30\x70\x78\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\ -\x22\x30\x20\x30\x20\x32\x34\x20\x32\x34\x22\x20\x66\x69\x6c\x6c\ -\x3d\x22\x6e\x6f\x6e\x65\x22\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\ -\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\ -\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x3e\x0a\x3c\x70\x61\x74\ -\x68\x20\x64\x3d\x22\x4d\x32\x30\x20\x39\x2e\x35\x30\x31\x39\x35\ -\x56\x38\x2e\x37\x34\x39\x38\x35\x43\x32\x30\x20\x37\x2e\x35\x30\ -\x37\x32\x31\x20\x31\x38\x2e\x39\x39\x32\x36\x20\x36\x2e\x34\x39\ -\x39\x38\x35\x20\x31\x37\x2e\x37\x35\x20\x36\x2e\x34\x39\x39\x38\ -\x35\x48\x31\x32\x2e\x30\x32\x34\x37\x4c\x39\x2e\x36\x34\x33\x36\ -\x38\x20\x34\x2e\x35\x31\x39\x39\x35\x43\x39\x2e\x32\x33\x39\x35\ -\x39\x20\x34\x2e\x31\x38\x33\x39\x33\x20\x38\x2e\x37\x33\x30\x36\ -\x33\x20\x33\x2e\x39\x39\x39\x39\x37\x20\x38\x2e\x32\x30\x35\x30\ -\x39\x20\x33\x2e\x39\x39\x39\x39\x37\x48\x34\x2e\x32\x34\x39\x35\ -\x37\x43\x33\x2e\x30\x30\x37\x32\x34\x20\x33\x2e\x39\x39\x39\x39\ -\x37\x20\x32\x20\x35\x2e\x30\x30\x36\x38\x36\x20\x31\x2e\x39\x39\ -\x39\x35\x37\x20\x36\x2e\x32\x34\x39\x31\x39\x4c\x31\x2e\x39\x39\ -\x35\x36\x31\x20\x31\x37\x2e\x37\x34\x39\x32\x43\x31\x2e\x39\x39\ -\x35\x31\x38\x20\x31\x38\x2e\x39\x39\x32\x31\x20\x33\x2e\x30\x30\ -\x32\x36\x36\x20\x32\x30\x20\x34\x2e\x32\x34\x35\x36\x31\x20\x32\ -\x30\x48\x34\x2e\x32\x37\x31\x39\x36\x43\x34\x2e\x32\x37\x36\x30\ -\x37\x20\x32\x30\x20\x34\x2e\x32\x38\x30\x31\x39\x20\x32\x30\x20\ -\x34\x2e\x32\x38\x34\x33\x31\x20\x32\x30\x48\x31\x38\x2e\x34\x36\ -\x39\x33\x43\x31\x39\x2e\x32\x37\x32\x33\x20\x32\x30\x20\x31\x39\ -\x2e\x39\x37\x32\x33\x20\x31\x39\x2e\x34\x35\x33\x35\x20\x32\x30\ -\x2e\x31\x36\x37\x20\x31\x38\x2e\x36\x37\x34\x35\x4c\x32\x31\x2e\ -\x39\x31\x36\x39\x20\x31\x31\x2e\x36\x37\x36\x35\x43\x32\x32\x2e\ -\x31\x39\x33\x31\x20\x31\x30\x2e\x35\x37\x31\x39\x20\x32\x31\x2e\ -\x33\x35\x37\x37\x20\x39\x2e\x35\x30\x31\x39\x35\x20\x32\x30\x2e\ -\x32\x31\x39\x32\x20\x39\x2e\x35\x30\x31\x39\x35\x48\x32\x30\x5a\ -\x4d\x34\x2e\x32\x34\x39\x35\x37\x20\x35\x2e\x34\x39\x39\x39\x37\ -\x48\x38\x2e\x32\x30\x35\x30\x39\x43\x38\x2e\x33\x38\x30\x32\x37\ -\x20\x35\x2e\x34\x39\x39\x39\x37\x20\x38\x2e\x35\x34\x39\x39\x33\ -\x20\x35\x2e\x35\x36\x31\x32\x39\x20\x38\x2e\x36\x38\x34\x36\x32\ -\x20\x35\x2e\x36\x37\x33\x33\x4c\x31\x31\x2e\x32\x37\x34\x31\x20\ -\x37\x2e\x38\x32\x36\x35\x32\x43\x31\x31\x2e\x34\x30\x38\x38\x20\ -\x37\x2e\x39\x33\x38\x35\x32\x20\x31\x31\x2e\x35\x37\x38\x34\x20\ -\x37\x2e\x39\x39\x39\x38\x35\x20\x31\x31\x2e\x37\x35\x33\x36\x20\ -\x37\x2e\x39\x39\x39\x38\x35\x48\x31\x37\x2e\x37\x35\x43\x31\x38\ -\x2e\x31\x36\x34\x32\x20\x37\x2e\x39\x39\x39\x38\x35\x20\x31\x38\ -\x2e\x35\x20\x38\x2e\x33\x33\x35\x36\x33\x20\x31\x38\x2e\x35\x20\ -\x38\x2e\x37\x34\x39\x38\x35\x56\x39\x2e\x35\x30\x31\x39\x35\x48\ -\x36\x2e\x34\x32\x33\x38\x35\x43\x35\x2e\x33\x39\x31\x33\x36\x20\ -\x39\x2e\x35\x30\x31\x39\x35\x20\x34\x2e\x34\x39\x31\x33\x37\x20\ -\x31\x30\x2e\x32\x30\x34\x37\x20\x34\x2e\x32\x34\x31\x20\x31\x31\ -\x2e\x32\x30\x36\x34\x4c\x33\x2e\x34\x39\x36\x38\x34\x20\x31\x34\ -\x2e\x31\x38\x33\x37\x4c\x33\x2e\x34\x39\x39\x35\x37\x20\x36\x2e\ -\x32\x34\x39\x37\x31\x43\x33\x2e\x34\x39\x39\x37\x31\x20\x35\x2e\ -\x38\x33\x35\x36\x20\x33\x2e\x38\x33\x35\x34\x36\x20\x35\x2e\x34\ -\x39\x39\x39\x37\x20\x34\x2e\x32\x34\x39\x35\x37\x20\x35\x2e\x34\ -\x39\x39\x39\x37\x5a\x4d\x35\x2e\x36\x39\x36\x32\x33\x20\x31\x31\ -\x2e\x35\x37\x30\x31\x43\x35\x2e\x37\x37\x39\x36\x39\x20\x31\x31\ -\x2e\x32\x33\x36\x32\x20\x36\x2e\x30\x37\x39\x36\x39\x20\x31\x31\ -\x2e\x30\x30\x32\x20\x36\x2e\x34\x32\x33\x38\x35\x20\x31\x31\x2e\ -\x30\x30\x32\x48\x32\x30\x2e\x32\x31\x39\x32\x43\x32\x30\x2e\x33\ -\x38\x31\x39\x20\x31\x31\x2e\x30\x30\x32\x20\x32\x30\x2e\x35\x30\ -\x31\x32\x20\x31\x31\x2e\x31\x35\x34\x38\x20\x32\x30\x2e\x34\x36\ -\x31\x37\x20\x31\x31\x2e\x33\x31\x32\x36\x4c\x31\x38\x2e\x37\x31\ -\x31\x39\x20\x31\x38\x2e\x33\x31\x30\x37\x43\x31\x38\x2e\x36\x38\ -\x34\x20\x31\x38\x2e\x34\x32\x31\x39\x20\x31\x38\x2e\x35\x38\x34\ -\x20\x31\x38\x2e\x35\x20\x31\x38\x2e\x34\x36\x39\x33\x20\x31\x38\ -\x2e\x35\x48\x34\x2e\x32\x38\x34\x33\x31\x43\x34\x2e\x31\x32\x31\ -\x36\x37\x20\x31\x38\x2e\x35\x20\x34\x2e\x30\x30\x32\x33\x33\x20\ -\x31\x38\x2e\x33\x34\x37\x32\x20\x34\x2e\x30\x34\x31\x37\x37\x20\ -\x31\x38\x2e\x31\x38\x39\x34\x4c\x35\x2e\x36\x39\x36\x32\x33\x20\ -\x31\x31\x2e\x35\x37\x30\x31\x5a\x22\x20\x66\x69\x6c\x6c\x3d\x22\ -\x23\x32\x31\x32\x31\x32\x31\x22\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\ -\x3e\ -\x00\x00\x09\x8f\ -\x3c\ -\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ -\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ -\x2d\x38\x22\x20\x73\x74\x61\x6e\x64\x61\x6c\x6f\x6e\x65\x3d\x22\ -\x6e\x6f\x22\x3f\x3e\x0a\x3c\x73\x76\x67\x0a\x20\x20\x20\x78\x6d\ -\x6c\x6e\x73\x3a\x64\x63\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x70\ -\x75\x72\x6c\x2e\x6f\x72\x67\x2f\x64\x63\x2f\x65\x6c\x65\x6d\x65\ -\x6e\x74\x73\x2f\x31\x2e\x31\x2f\x22\x0a\x20\x20\x20\x78\x6d\x6c\ -\x6e\x73\x3a\x63\x63\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x63\x72\ -\x65\x61\x74\x69\x76\x65\x63\x6f\x6d\x6d\x6f\x6e\x73\x2e\x6f\x72\ -\x67\x2f\x6e\x73\x23\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\ -\x72\x64\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\ -\x77\x33\x2e\x6f\x72\x67\x2f\x31\x39\x39\x39\x2f\x30\x32\x2f\x32\ -\x32\x2d\x72\x64\x66\x2d\x73\x79\x6e\x74\x61\x78\x2d\x6e\x73\x23\ -\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x73\x76\x67\x3d\x22\ -\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\ -\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x0a\x20\x20\x20\x78\ -\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\ -\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\ -\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x73\x6f\x64\x69\x70\ -\x6f\x64\x69\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x73\x6f\x64\x69\ -\x70\x6f\x64\x69\x2e\x73\x6f\x75\x72\x63\x65\x66\x6f\x72\x67\x65\ -\x2e\x6e\x65\x74\x2f\x44\x54\x44\x2f\x73\x6f\x64\x69\x70\x6f\x64\ -\x69\x2d\x30\x2e\x64\x74\x64\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\ -\x73\x3a\x69\x6e\x6b\x73\x63\x61\x70\x65\x3d\x22\x68\x74\x74\x70\ -\x3a\x2f\x2f\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\x65\x2e\ -\x6f\x72\x67\x2f\x6e\x61\x6d\x65\x73\x70\x61\x63\x65\x73\x2f\x69\ -\x6e\x6b\x73\x63\x61\x70\x65\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\ -\x63\x61\x70\x65\x3a\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ -\x30\x72\x63\x31\x20\x28\x30\x39\x39\x36\x30\x64\x36\x66\x30\x35\ -\x2c\x20\x32\x30\x32\x30\x2d\x30\x34\x2d\x30\x39\x29\x22\x0a\x20\ -\x20\x20\x73\x6f\x64\x69\x70\x6f\x64\x69\x3a\x64\x6f\x63\x6e\x61\ -\x6d\x65\x3d\x22\x63\x6f\x6c\x6c\x61\x70\x73\x65\x2e\x73\x76\x67\ -\x22\x0a\x20\x20\x20\x69\x64\x3d\x22\x73\x76\x67\x36\x22\x0a\x20\ -\x20\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\x31\x22\x0a\ -\x20\x20\x20\x63\x6c\x61\x73\x73\x3d\x22\x66\x65\x61\x74\x68\x65\ -\x72\x20\x66\x65\x61\x74\x68\x65\x72\x2d\x63\x6f\x64\x65\x22\x0a\ -\x20\x20\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\x6a\x6f\ -\x69\x6e\x3d\x22\x72\x6f\x75\x6e\x64\x22\x0a\x20\x20\x20\x73\x74\ -\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\x63\x61\x70\x3d\x22\x72\x6f\ -\x75\x6e\x64\x22\x0a\x20\x20\x20\x73\x74\x72\x6f\x6b\x65\x2d\x77\ -\x69\x64\x74\x68\x3d\x22\x32\x22\x0a\x20\x20\x20\x73\x74\x72\x6f\ -\x6b\x65\x3d\x22\x63\x75\x72\x72\x65\x6e\x74\x43\x6f\x6c\x6f\x72\ -\x22\x0a\x20\x20\x20\x66\x69\x6c\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\ -\x0a\x20\x20\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\x30\ -\x20\x32\x34\x20\x32\x34\x22\x0a\x20\x20\x20\x68\x65\x69\x67\x68\ -\x74\x3d\x22\x32\x34\x22\x0a\x20\x20\x20\x77\x69\x64\x74\x68\x3d\ -\x22\x32\x34\x22\x3e\x0a\x20\x20\x3c\x6d\x65\x74\x61\x64\x61\x74\ -\x61\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x6d\x65\x74\x61\x64\ -\x61\x74\x61\x31\x32\x22\x3e\x0a\x20\x20\x20\x20\x3c\x72\x64\x66\ -\x3a\x52\x44\x46\x3e\x0a\x20\x20\x20\x20\x20\x20\x3c\x63\x63\x3a\ -\x57\x6f\x72\x6b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x72\x64\ -\x66\x3a\x61\x62\x6f\x75\x74\x3d\x22\x22\x3e\x0a\x20\x20\x20\x20\ -\x20\x20\x20\x20\x3c\x64\x63\x3a\x66\x6f\x72\x6d\x61\x74\x3e\x69\ -\x6d\x61\x67\x65\x2f\x73\x76\x67\x2b\x78\x6d\x6c\x3c\x2f\x64\x63\ -\x3a\x66\x6f\x72\x6d\x61\x74\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\ -\x20\x3c\x64\x63\x3a\x74\x79\x70\x65\x0a\x20\x20\x20\x20\x20\x20\ -\x20\x20\x20\x20\x20\x72\x64\x66\x3a\x72\x65\x73\x6f\x75\x72\x63\ -\x65\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x70\x75\x72\x6c\x2e\x6f\ -\x72\x67\x2f\x64\x63\x2f\x64\x63\x6d\x69\x74\x79\x70\x65\x2f\x53\ -\x74\x69\x6c\x6c\x49\x6d\x61\x67\x65\x22\x20\x2f\x3e\x0a\x20\x20\ -\x20\x20\x20\x20\x20\x20\x3c\x64\x63\x3a\x74\x69\x74\x6c\x65\x3e\ -\x3c\x2f\x64\x63\x3a\x74\x69\x74\x6c\x65\x3e\x0a\x20\x20\x20\x20\ -\x20\x20\x3c\x2f\x63\x63\x3a\x57\x6f\x72\x6b\x3e\x0a\x20\x20\x20\ -\x20\x3c\x2f\x72\x64\x66\x3a\x52\x44\x46\x3e\x0a\x20\x20\x3c\x2f\ -\x6d\x65\x74\x61\x64\x61\x74\x61\x3e\x0a\x20\x20\x3c\x64\x65\x66\ -\x73\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x64\x65\x66\x73\x31\ -\x30\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x73\x6f\x64\x69\x70\x6f\x64\ -\x69\x3a\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\x0a\x20\x20\x20\x20\ -\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x75\x72\x72\x65\x6e\ -\x74\x2d\x6c\x61\x79\x65\x72\x3d\x22\x73\x76\x67\x36\x22\x0a\x20\ -\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\ -\x64\x6f\x77\x2d\x6d\x61\x78\x69\x6d\x69\x7a\x65\x64\x3d\x22\x30\ -\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\ -\x77\x69\x6e\x64\x6f\x77\x2d\x79\x3d\x22\x31\x38\x35\x22\x0a\x20\ -\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\ -\x64\x6f\x77\x2d\x78\x3d\x22\x31\x33\x38\x37\x22\x0a\x20\x20\x20\ -\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x79\x3d\x22\x31\ -\x32\x2e\x31\x38\x37\x33\x38\x32\x22\x0a\x20\x20\x20\x20\x20\x69\ -\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x78\x3d\x22\x31\x32\x2e\x34\ -\x37\x33\x33\x38\x37\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\ -\x63\x61\x70\x65\x3a\x7a\x6f\x6f\x6d\x3d\x22\x32\x39\x2e\x36\x39\ -\x38\x34\x38\x35\x22\x0a\x20\x20\x20\x20\x20\x73\x68\x6f\x77\x67\ -\x72\x69\x64\x3d\x22\x66\x61\x6c\x73\x65\x22\x0a\x20\x20\x20\x20\ -\x20\x69\x64\x3d\x22\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\x38\x22\ -\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\ -\x69\x6e\x64\x6f\x77\x2d\x68\x65\x69\x67\x68\x74\x3d\x22\x31\x31\ -\x32\x38\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\ -\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x77\x69\x64\x74\x68\x3d\x22\ -\x31\x39\x37\x34\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\ -\x61\x70\x65\x3a\x70\x61\x67\x65\x73\x68\x61\x64\x6f\x77\x3d\x22\ -\x32\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\ -\x3a\x70\x61\x67\x65\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\x30\x22\ -\x0a\x20\x20\x20\x20\x20\x67\x75\x69\x64\x65\x74\x6f\x6c\x65\x72\ -\x61\x6e\x63\x65\x3d\x22\x31\x30\x22\x0a\x20\x20\x20\x20\x20\x67\ -\x72\x69\x64\x74\x6f\x6c\x65\x72\x61\x6e\x63\x65\x3d\x22\x31\x30\ -\x22\x0a\x20\x20\x20\x20\x20\x6f\x62\x6a\x65\x63\x74\x74\x6f\x6c\ -\x65\x72\x61\x6e\x63\x65\x3d\x22\x31\x30\x22\x0a\x20\x20\x20\x20\ -\x20\x62\x6f\x72\x64\x65\x72\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\ -\x31\x22\x0a\x20\x20\x20\x20\x20\x62\x6f\x72\x64\x65\x72\x63\x6f\ -\x6c\x6f\x72\x3d\x22\x23\x36\x36\x36\x36\x36\x36\x22\x0a\x20\x20\ -\x20\x20\x20\x70\x61\x67\x65\x63\x6f\x6c\x6f\x72\x3d\x22\x23\x66\ -\x66\x66\x66\x66\x66\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x70\x61\x74\ -\x68\x0a\x20\x20\x20\x20\x20\x73\x74\x79\x6c\x65\x3d\x22\x66\x69\ -\x6c\x6c\x3a\x6e\x6f\x6e\x65\x3b\x73\x74\x72\x6f\x6b\x65\x3a\x23\ -\x30\x30\x30\x30\x30\x30\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\ -\x64\x74\x68\x3a\x31\x2e\x38\x39\x34\x34\x33\x3b\x73\x74\x72\x6f\ -\x6b\x65\x2d\x6c\x69\x6e\x65\x63\x61\x70\x3a\x72\x6f\x75\x6e\x64\ -\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\x6a\x6f\x69\x6e\ -\x3a\x6d\x69\x74\x65\x72\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x6d\x69\ -\x74\x65\x72\x6c\x69\x6d\x69\x74\x3a\x34\x3b\x73\x74\x72\x6f\x6b\ -\x65\x2d\x64\x61\x73\x68\x61\x72\x72\x61\x79\x3a\x6e\x6f\x6e\x65\ -\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x6f\x70\x61\x63\x69\x74\x79\x3a\ -\x31\x22\x0a\x20\x20\x20\x20\x20\x64\x3d\x22\x4d\x20\x32\x2e\x38\ -\x39\x35\x37\x37\x30\x36\x2c\x33\x2e\x37\x33\x37\x35\x36\x34\x34\ -\x20\x48\x20\x32\x30\x2e\x32\x33\x36\x37\x32\x33\x22\x0a\x20\x20\ -\x20\x20\x20\x69\x64\x3d\x22\x70\x61\x74\x68\x38\x35\x37\x22\x0a\ -\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x6f\ -\x6e\x6e\x65\x63\x74\x6f\x72\x2d\x63\x75\x72\x76\x61\x74\x75\x72\ -\x65\x3d\x22\x30\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x70\x61\x74\x68\ -\x0a\x20\x20\x20\x20\x20\x73\x74\x79\x6c\x65\x3d\x22\x66\x69\x6c\ -\x6c\x3a\x6e\x6f\x6e\x65\x3b\x73\x74\x72\x6f\x6b\x65\x3a\x23\x30\ -\x30\x30\x30\x30\x30\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\ -\x74\x68\x3a\x31\x2e\x38\x39\x34\x34\x33\x3b\x73\x74\x72\x6f\x6b\ -\x65\x2d\x6c\x69\x6e\x65\x63\x61\x70\x3a\x72\x6f\x75\x6e\x64\x3b\ -\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\x6a\x6f\x69\x6e\x3a\ -\x6d\x69\x74\x65\x72\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x6d\x69\x74\ -\x65\x72\x6c\x69\x6d\x69\x74\x3a\x34\x3b\x73\x74\x72\x6f\x6b\x65\ -\x2d\x64\x61\x73\x68\x61\x72\x72\x61\x79\x3a\x6e\x6f\x6e\x65\x3b\ -\x73\x74\x72\x6f\x6b\x65\x2d\x6f\x70\x61\x63\x69\x74\x79\x3a\x31\ -\x22\x0a\x20\x20\x20\x20\x20\x64\x3d\x22\x4d\x20\x32\x2e\x38\x39\ -\x35\x37\x37\x30\x36\x2c\x31\x31\x2e\x34\x33\x37\x31\x37\x32\x20\ -\x48\x20\x32\x30\x2e\x32\x33\x36\x37\x32\x33\x22\x0a\x20\x20\x20\ -\x20\x20\x69\x64\x3d\x22\x70\x61\x74\x68\x38\x35\x39\x22\x0a\x20\ -\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x6f\x6e\ -\x6e\x65\x63\x74\x6f\x72\x2d\x63\x75\x72\x76\x61\x74\x75\x72\x65\ -\x3d\x22\x30\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x70\x61\x74\x68\x0a\ -\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x6f\ -\x6e\x6e\x65\x63\x74\x6f\x72\x2d\x63\x75\x72\x76\x61\x74\x75\x72\ -\x65\x3d\x22\x30\x22\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x70\ -\x61\x74\x68\x38\x35\x35\x22\x0a\x20\x20\x20\x20\x20\x64\x3d\x22\ -\x4d\x20\x32\x2e\x38\x39\x35\x37\x37\x30\x36\x2c\x37\x2e\x35\x38\ -\x37\x33\x36\x38\x31\x20\x48\x20\x32\x30\x2e\x32\x33\x36\x37\x32\ -\x33\x22\x0a\x20\x20\x20\x20\x20\x73\x74\x79\x6c\x65\x3d\x22\x66\ -\x69\x6c\x6c\x3a\x6e\x6f\x6e\x65\x3b\x73\x74\x72\x6f\x6b\x65\x3a\ -\x23\x30\x30\x30\x30\x30\x30\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x77\ -\x69\x64\x74\x68\x3a\x31\x2e\x38\x39\x34\x34\x33\x3b\x73\x74\x72\ -\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\x63\x61\x70\x3a\x72\x6f\x75\x6e\ -\x64\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\x6a\x6f\x69\ -\x6e\x3a\x6d\x69\x74\x65\x72\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x6d\ -\x69\x74\x65\x72\x6c\x69\x6d\x69\x74\x3a\x34\x3b\x73\x74\x72\x6f\ -\x6b\x65\x2d\x64\x61\x73\x68\x61\x72\x72\x61\x79\x3a\x6e\x6f\x6e\ -\x65\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x6f\x70\x61\x63\x69\x74\x79\ -\x3a\x31\x22\x20\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\x3e\x0a\ -\x00\x00\x07\x9d\ -\x3c\ -\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ -\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ -\x2d\x38\x22\x20\x73\x74\x61\x6e\x64\x61\x6c\x6f\x6e\x65\x3d\x22\ -\x6e\x6f\x22\x3f\x3e\x0a\x3c\x73\x76\x67\x0a\x20\x20\x20\x78\x6d\ -\x6c\x6e\x73\x3a\x64\x63\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x70\ -\x75\x72\x6c\x2e\x6f\x72\x67\x2f\x64\x63\x2f\x65\x6c\x65\x6d\x65\ -\x6e\x74\x73\x2f\x31\x2e\x31\x2f\x22\x0a\x20\x20\x20\x78\x6d\x6c\ -\x6e\x73\x3a\x63\x63\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x63\x72\ -\x65\x61\x74\x69\x76\x65\x63\x6f\x6d\x6d\x6f\x6e\x73\x2e\x6f\x72\ -\x67\x2f\x6e\x73\x23\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\ -\x72\x64\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\ -\x77\x33\x2e\x6f\x72\x67\x2f\x31\x39\x39\x39\x2f\x30\x32\x2f\x32\ -\x32\x2d\x72\x64\x66\x2d\x73\x79\x6e\x74\x61\x78\x2d\x6e\x73\x23\ -\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x73\x76\x67\x3d\x22\ -\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\ -\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x0a\x20\x20\x20\x78\ -\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\ -\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\ -\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x73\x6f\x64\x69\x70\ -\x6f\x64\x69\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x73\x6f\x64\x69\ -\x70\x6f\x64\x69\x2e\x73\x6f\x75\x72\x63\x65\x66\x6f\x72\x67\x65\ -\x2e\x6e\x65\x74\x2f\x44\x54\x44\x2f\x73\x6f\x64\x69\x70\x6f\x64\ -\x69\x2d\x30\x2e\x64\x74\x64\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\ -\x73\x3a\x69\x6e\x6b\x73\x63\x61\x70\x65\x3d\x22\x68\x74\x74\x70\ -\x3a\x2f\x2f\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\x65\x2e\ -\x6f\x72\x67\x2f\x6e\x61\x6d\x65\x73\x70\x61\x63\x65\x73\x2f\x69\ -\x6e\x6b\x73\x63\x61\x70\x65\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\ -\x63\x61\x70\x65\x3a\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ -\x30\x72\x63\x31\x20\x28\x30\x39\x39\x36\x30\x64\x36\x66\x30\x35\ -\x2c\x20\x32\x30\x32\x30\x2d\x30\x34\x2d\x30\x39\x29\x22\x0a\x20\ -\x20\x20\x73\x6f\x64\x69\x70\x6f\x64\x69\x3a\x64\x6f\x63\x6e\x61\ -\x6d\x65\x3d\x22\x61\x6c\x65\x72\x74\x2d\x6f\x63\x74\x61\x67\x6f\ -\x6e\x2d\x72\x65\x64\x2e\x73\x76\x67\x22\x0a\x20\x20\x20\x69\x64\ -\x3d\x22\x73\x76\x67\x38\x22\x0a\x20\x20\x20\x76\x65\x72\x73\x69\ -\x6f\x6e\x3d\x22\x31\x2e\x31\x22\x0a\x20\x20\x20\x63\x6c\x61\x73\ -\x73\x3d\x22\x66\x65\x61\x74\x68\x65\x72\x20\x66\x65\x61\x74\x68\ -\x65\x72\x2d\x61\x6c\x65\x72\x74\x2d\x6f\x63\x74\x61\x67\x6f\x6e\ -\x22\x0a\x20\x20\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\ -\x6a\x6f\x69\x6e\x3d\x22\x72\x6f\x75\x6e\x64\x22\x0a\x20\x20\x20\ -\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\x63\x61\x70\x3d\x22\ -\x72\x6f\x75\x6e\x64\x22\x0a\x20\x20\x20\x73\x74\x72\x6f\x6b\x65\ -\x2d\x77\x69\x64\x74\x68\x3d\x22\x32\x22\x0a\x20\x20\x20\x73\x74\ -\x72\x6f\x6b\x65\x3d\x22\x63\x75\x72\x72\x65\x6e\x74\x43\x6f\x6c\ -\x6f\x72\x22\x0a\x20\x20\x20\x66\x69\x6c\x6c\x3d\x22\x6e\x6f\x6e\ -\x65\x22\x0a\x20\x20\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\x30\ -\x20\x30\x20\x32\x34\x20\x32\x34\x22\x0a\x20\x20\x20\x68\x65\x69\ -\x67\x68\x74\x3d\x22\x32\x34\x22\x0a\x20\x20\x20\x77\x69\x64\x74\ -\x68\x3d\x22\x32\x34\x22\x3e\x0a\x20\x20\x3c\x6d\x65\x74\x61\x64\ -\x61\x74\x61\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x6d\x65\x74\ -\x61\x64\x61\x74\x61\x31\x34\x22\x3e\x0a\x20\x20\x20\x20\x3c\x72\ -\x64\x66\x3a\x52\x44\x46\x3e\x0a\x20\x20\x20\x20\x20\x20\x3c\x63\ -\x63\x3a\x57\x6f\x72\x6b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\ -\x72\x64\x66\x3a\x61\x62\x6f\x75\x74\x3d\x22\x22\x3e\x0a\x20\x20\ -\x20\x20\x20\x20\x20\x20\x3c\x64\x63\x3a\x66\x6f\x72\x6d\x61\x74\ -\x3e\x69\x6d\x61\x67\x65\x2f\x73\x76\x67\x2b\x78\x6d\x6c\x3c\x2f\ -\x64\x63\x3a\x66\x6f\x72\x6d\x61\x74\x3e\x0a\x20\x20\x20\x20\x20\ -\x20\x20\x20\x3c\x64\x63\x3a\x74\x79\x70\x65\x0a\x20\x20\x20\x20\ -\x20\x20\x20\x20\x20\x20\x20\x72\x64\x66\x3a\x72\x65\x73\x6f\x75\ -\x72\x63\x65\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x70\x75\x72\x6c\ -\x2e\x6f\x72\x67\x2f\x64\x63\x2f\x64\x63\x6d\x69\x74\x79\x70\x65\ -\x2f\x53\x74\x69\x6c\x6c\x49\x6d\x61\x67\x65\x22\x20\x2f\x3e\x0a\ -\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x63\x3a\x74\x69\x74\x6c\ -\x65\x3e\x3c\x2f\x64\x63\x3a\x74\x69\x74\x6c\x65\x3e\x0a\x20\x20\ -\x20\x20\x20\x20\x3c\x2f\x63\x63\x3a\x57\x6f\x72\x6b\x3e\x0a\x20\ -\x20\x20\x20\x3c\x2f\x72\x64\x66\x3a\x52\x44\x46\x3e\x0a\x20\x20\ -\x3c\x2f\x6d\x65\x74\x61\x64\x61\x74\x61\x3e\x0a\x20\x20\x3c\x64\ -\x65\x66\x73\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x64\x65\x66\ -\x73\x31\x32\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x73\x6f\x64\x69\x70\ -\x6f\x64\x69\x3a\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\x0a\x20\x20\ -\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x75\x72\x72\ -\x65\x6e\x74\x2d\x6c\x61\x79\x65\x72\x3d\x22\x73\x76\x67\x38\x22\ -\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\ -\x69\x6e\x64\x6f\x77\x2d\x6d\x61\x78\x69\x6d\x69\x7a\x65\x64\x3d\ -\x22\x30\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\ -\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x79\x3d\x22\x32\x38\x31\x22\ -\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\ -\x69\x6e\x64\x6f\x77\x2d\x78\x3d\x22\x31\x34\x33\x33\x22\x0a\x20\ -\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x79\x3d\ -\x22\x31\x32\x2e\x34\x30\x39\x37\x39\x37\x22\x0a\x20\x20\x20\x20\ -\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x78\x3d\x22\x31\x32\ -\x2e\x38\x32\x38\x32\x31\x36\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\ -\x6b\x73\x63\x61\x70\x65\x3a\x7a\x6f\x6f\x6d\x3d\x22\x32\x31\x22\ -\x0a\x20\x20\x20\x20\x20\x73\x68\x6f\x77\x67\x72\x69\x64\x3d\x22\ -\x66\x61\x6c\x73\x65\x22\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\ -\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\x31\x30\x22\x0a\x20\x20\x20\ -\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\ -\x77\x2d\x68\x65\x69\x67\x68\x74\x3d\x22\x39\x35\x39\x22\x0a\x20\ -\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\ -\x64\x6f\x77\x2d\x77\x69\x64\x74\x68\x3d\x22\x31\x35\x34\x32\x22\ -\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x70\ -\x61\x67\x65\x73\x68\x61\x64\x6f\x77\x3d\x22\x32\x22\x0a\x20\x20\ -\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x70\x61\x67\x65\ -\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\x30\x22\x0a\x20\x20\x20\x20\ -\x20\x67\x75\x69\x64\x65\x74\x6f\x6c\x65\x72\x61\x6e\x63\x65\x3d\ -\x22\x31\x30\x22\x0a\x20\x20\x20\x20\x20\x67\x72\x69\x64\x74\x6f\ -\x6c\x65\x72\x61\x6e\x63\x65\x3d\x22\x31\x30\x22\x0a\x20\x20\x20\ -\x20\x20\x6f\x62\x6a\x65\x63\x74\x74\x6f\x6c\x65\x72\x61\x6e\x63\ -\x65\x3d\x22\x31\x30\x22\x0a\x20\x20\x20\x20\x20\x62\x6f\x72\x64\ -\x65\x72\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\x31\x22\x0a\x20\x20\ -\x20\x20\x20\x62\x6f\x72\x64\x65\x72\x63\x6f\x6c\x6f\x72\x3d\x22\ -\x23\x36\x36\x36\x36\x36\x36\x22\x0a\x20\x20\x20\x20\x20\x70\x61\ -\x67\x65\x63\x6f\x6c\x6f\x72\x3d\x22\x23\x66\x66\x66\x66\x66\x66\ -\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x70\x6f\x6c\x79\x67\x6f\x6e\x0a\ -\x20\x20\x20\x20\x20\x73\x74\x79\x6c\x65\x3d\x22\x66\x69\x6c\x6c\ -\x3a\x23\x30\x30\x66\x66\x30\x30\x22\x0a\x20\x20\x20\x20\x20\x69\ -\x64\x3d\x22\x70\x6f\x6c\x79\x67\x6f\x6e\x32\x22\x0a\x20\x20\x20\ -\x20\x20\x70\x6f\x69\x6e\x74\x73\x3d\x22\x37\x2e\x38\x36\x20\x32\ -\x20\x31\x36\x2e\x31\x34\x20\x32\x20\x32\x32\x20\x37\x2e\x38\x36\ -\x20\x32\x32\x20\x31\x36\x2e\x31\x34\x20\x31\x36\x2e\x31\x34\x20\ -\x32\x32\x20\x37\x2e\x38\x36\x20\x32\x32\x20\x32\x20\x31\x36\x2e\ -\x31\x34\x20\x32\x20\x37\x2e\x38\x36\x20\x37\x2e\x38\x36\x20\x32\ -\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x6c\x69\x6e\x65\x0a\x20\x20\x20\ -\x20\x20\x69\x64\x3d\x22\x6c\x69\x6e\x65\x34\x22\x0a\x20\x20\x20\ -\x20\x20\x79\x32\x3d\x22\x31\x32\x22\x0a\x20\x20\x20\x20\x20\x78\ -\x32\x3d\x22\x31\x32\x22\x0a\x20\x20\x20\x20\x20\x79\x31\x3d\x22\ -\x38\x22\x0a\x20\x20\x20\x20\x20\x78\x31\x3d\x22\x31\x32\x22\x20\ -\x2f\x3e\x0a\x20\x20\x3c\x6c\x69\x6e\x65\x0a\x20\x20\x20\x20\x20\ -\x69\x64\x3d\x22\x6c\x69\x6e\x65\x36\x22\x0a\x20\x20\x20\x20\x20\ -\x79\x32\x3d\x22\x31\x36\x22\x0a\x20\x20\x20\x20\x20\x78\x32\x3d\ -\x22\x31\x32\x2e\x30\x31\x22\x0a\x20\x20\x20\x20\x20\x79\x31\x3d\ -\x22\x31\x36\x22\x0a\x20\x20\x20\x20\x20\x78\x31\x3d\x22\x31\x32\ -\x22\x20\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\x3e\x0a\ -\x00\x00\x01\x90\ -\x3c\ -\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ -\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ -\x30\x2f\x73\x76\x67\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x32\x34\ -\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x32\x34\x22\x20\x76\x69\ -\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\x30\x20\x32\x34\x20\x32\x34\ -\x22\x20\x66\x69\x6c\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\x20\x73\x74\ -\x72\x6f\x6b\x65\x3d\x22\x63\x75\x72\x72\x65\x6e\x74\x43\x6f\x6c\ -\x6f\x72\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\x68\ -\x3d\x22\x32\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\ -\x63\x61\x70\x3d\x22\x72\x6f\x75\x6e\x64\x22\x20\x73\x74\x72\x6f\ -\x6b\x65\x2d\x6c\x69\x6e\x65\x6a\x6f\x69\x6e\x3d\x22\x72\x6f\x75\ -\x6e\x64\x22\x20\x63\x6c\x61\x73\x73\x3d\x22\x66\x65\x61\x74\x68\ -\x65\x72\x20\x66\x65\x61\x74\x68\x65\x72\x2d\x72\x65\x66\x72\x65\ -\x73\x68\x2d\x63\x77\x22\x3e\x3c\x70\x6f\x6c\x79\x6c\x69\x6e\x65\ -\x20\x70\x6f\x69\x6e\x74\x73\x3d\x22\x32\x33\x20\x34\x20\x32\x33\ -\x20\x31\x30\x20\x31\x37\x20\x31\x30\x22\x3e\x3c\x2f\x70\x6f\x6c\ -\x79\x6c\x69\x6e\x65\x3e\x3c\x70\x6f\x6c\x79\x6c\x69\x6e\x65\x20\ -\x70\x6f\x69\x6e\x74\x73\x3d\x22\x31\x20\x32\x30\x20\x31\x20\x31\ -\x34\x20\x37\x20\x31\x34\x22\x3e\x3c\x2f\x70\x6f\x6c\x79\x6c\x69\ -\x6e\x65\x3e\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\x33\x2e\x35\ -\x31\x20\x39\x61\x39\x20\x39\x20\x30\x20\x30\x20\x31\x20\x31\x34\ -\x2e\x38\x35\x2d\x33\x2e\x33\x36\x4c\x32\x33\x20\x31\x30\x4d\x31\ -\x20\x31\x34\x6c\x34\x2e\x36\x34\x20\x34\x2e\x33\x36\x41\x39\x20\ -\x39\x20\x30\x20\x30\x20\x30\x20\x32\x30\x2e\x34\x39\x20\x31\x35\ -\x22\x3e\x3c\x2f\x70\x61\x74\x68\x3e\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x01\x76\ -\x3c\ -\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ -\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ -\x30\x2f\x73\x76\x67\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x32\x34\ -\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x32\x34\x22\x20\x76\x69\ -\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\x30\x20\x32\x34\x20\x32\x34\ -\x22\x20\x66\x69\x6c\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\x20\x73\x74\ -\x72\x6f\x6b\x65\x3d\x22\x63\x75\x72\x72\x65\x6e\x74\x43\x6f\x6c\ -\x6f\x72\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\x68\ -\x3d\x22\x32\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\ -\x63\x61\x70\x3d\x22\x72\x6f\x75\x6e\x64\x22\x20\x73\x74\x72\x6f\ -\x6b\x65\x2d\x6c\x69\x6e\x65\x6a\x6f\x69\x6e\x3d\x22\x72\x6f\x75\ -\x6e\x64\x22\x20\x63\x6c\x61\x73\x73\x3d\x22\x66\x65\x61\x74\x68\ -\x65\x72\x20\x66\x65\x61\x74\x68\x65\x72\x2d\x64\x65\x6c\x65\x74\ -\x65\x22\x3e\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\x32\x31\x20\ -\x34\x48\x38\x6c\x2d\x37\x20\x38\x20\x37\x20\x38\x68\x31\x33\x61\ -\x32\x20\x32\x20\x30\x20\x30\x20\x30\x20\x32\x2d\x32\x56\x36\x61\ -\x32\x20\x32\x20\x30\x20\x30\x20\x30\x2d\x32\x2d\x32\x7a\x22\x3e\ -\x3c\x2f\x70\x61\x74\x68\x3e\x3c\x6c\x69\x6e\x65\x20\x78\x31\x3d\ -\x22\x31\x38\x22\x20\x79\x31\x3d\x22\x39\x22\x20\x78\x32\x3d\x22\ -\x31\x32\x22\x20\x79\x32\x3d\x22\x31\x35\x22\x3e\x3c\x2f\x6c\x69\ -\x6e\x65\x3e\x3c\x6c\x69\x6e\x65\x20\x78\x31\x3d\x22\x31\x32\x22\ -\x20\x79\x31\x3d\x22\x39\x22\x20\x78\x32\x3d\x22\x31\x38\x22\x20\ -\x79\x32\x3d\x22\x31\x35\x22\x3e\x3c\x2f\x6c\x69\x6e\x65\x3e\x3c\ -\x2f\x73\x76\x67\x3e\ -\x00\x00\x07\x9d\ -\x3c\ -\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ -\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ -\x2d\x38\x22\x20\x73\x74\x61\x6e\x64\x61\x6c\x6f\x6e\x65\x3d\x22\ -\x6e\x6f\x22\x3f\x3e\x0a\x3c\x73\x76\x67\x0a\x20\x20\x20\x78\x6d\ -\x6c\x6e\x73\x3a\x64\x63\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x70\ -\x75\x72\x6c\x2e\x6f\x72\x67\x2f\x64\x63\x2f\x65\x6c\x65\x6d\x65\ -\x6e\x74\x73\x2f\x31\x2e\x31\x2f\x22\x0a\x20\x20\x20\x78\x6d\x6c\ -\x6e\x73\x3a\x63\x63\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x63\x72\ -\x65\x61\x74\x69\x76\x65\x63\x6f\x6d\x6d\x6f\x6e\x73\x2e\x6f\x72\ -\x67\x2f\x6e\x73\x23\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\ -\x72\x64\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\ -\x77\x33\x2e\x6f\x72\x67\x2f\x31\x39\x39\x39\x2f\x30\x32\x2f\x32\ -\x32\x2d\x72\x64\x66\x2d\x73\x79\x6e\x74\x61\x78\x2d\x6e\x73\x23\ -\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x73\x76\x67\x3d\x22\ -\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\ -\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x0a\x20\x20\x20\x78\ -\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\ -\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\ -\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x73\x6f\x64\x69\x70\ -\x6f\x64\x69\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x73\x6f\x64\x69\ -\x70\x6f\x64\x69\x2e\x73\x6f\x75\x72\x63\x65\x66\x6f\x72\x67\x65\ -\x2e\x6e\x65\x74\x2f\x44\x54\x44\x2f\x73\x6f\x64\x69\x70\x6f\x64\ -\x69\x2d\x30\x2e\x64\x74\x64\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\ -\x73\x3a\x69\x6e\x6b\x73\x63\x61\x70\x65\x3d\x22\x68\x74\x74\x70\ -\x3a\x2f\x2f\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\x65\x2e\ -\x6f\x72\x67\x2f\x6e\x61\x6d\x65\x73\x70\x61\x63\x65\x73\x2f\x69\ -\x6e\x6b\x73\x63\x61\x70\x65\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\ -\x63\x61\x70\x65\x3a\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ -\x30\x72\x63\x31\x20\x28\x30\x39\x39\x36\x30\x64\x36\x66\x30\x35\ -\x2c\x20\x32\x30\x32\x30\x2d\x30\x34\x2d\x30\x39\x29\x22\x0a\x20\ -\x20\x20\x73\x6f\x64\x69\x70\x6f\x64\x69\x3a\x64\x6f\x63\x6e\x61\ -\x6d\x65\x3d\x22\x61\x6c\x65\x72\x74\x2d\x6f\x63\x74\x61\x67\x6f\ -\x6e\x2d\x72\x65\x64\x2e\x73\x76\x67\x22\x0a\x20\x20\x20\x69\x64\ -\x3d\x22\x73\x76\x67\x38\x22\x0a\x20\x20\x20\x76\x65\x72\x73\x69\ -\x6f\x6e\x3d\x22\x31\x2e\x31\x22\x0a\x20\x20\x20\x63\x6c\x61\x73\ -\x73\x3d\x22\x66\x65\x61\x74\x68\x65\x72\x20\x66\x65\x61\x74\x68\ -\x65\x72\x2d\x61\x6c\x65\x72\x74\x2d\x6f\x63\x74\x61\x67\x6f\x6e\ -\x22\x0a\x20\x20\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\ -\x6a\x6f\x69\x6e\x3d\x22\x72\x6f\x75\x6e\x64\x22\x0a\x20\x20\x20\ -\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\x63\x61\x70\x3d\x22\ -\x72\x6f\x75\x6e\x64\x22\x0a\x20\x20\x20\x73\x74\x72\x6f\x6b\x65\ -\x2d\x77\x69\x64\x74\x68\x3d\x22\x32\x22\x0a\x20\x20\x20\x73\x74\ -\x72\x6f\x6b\x65\x3d\x22\x63\x75\x72\x72\x65\x6e\x74\x43\x6f\x6c\ -\x6f\x72\x22\x0a\x20\x20\x20\x66\x69\x6c\x6c\x3d\x22\x6e\x6f\x6e\ -\x65\x22\x0a\x20\x20\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\x30\ -\x20\x30\x20\x32\x34\x20\x32\x34\x22\x0a\x20\x20\x20\x68\x65\x69\ -\x67\x68\x74\x3d\x22\x32\x34\x22\x0a\x20\x20\x20\x77\x69\x64\x74\ -\x68\x3d\x22\x32\x34\x22\x3e\x0a\x20\x20\x3c\x6d\x65\x74\x61\x64\ -\x61\x74\x61\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x6d\x65\x74\ -\x61\x64\x61\x74\x61\x31\x34\x22\x3e\x0a\x20\x20\x20\x20\x3c\x72\ -\x64\x66\x3a\x52\x44\x46\x3e\x0a\x20\x20\x20\x20\x20\x20\x3c\x63\ -\x63\x3a\x57\x6f\x72\x6b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\ -\x72\x64\x66\x3a\x61\x62\x6f\x75\x74\x3d\x22\x22\x3e\x0a\x20\x20\ -\x20\x20\x20\x20\x20\x20\x3c\x64\x63\x3a\x66\x6f\x72\x6d\x61\x74\ -\x3e\x69\x6d\x61\x67\x65\x2f\x73\x76\x67\x2b\x78\x6d\x6c\x3c\x2f\ -\x64\x63\x3a\x66\x6f\x72\x6d\x61\x74\x3e\x0a\x20\x20\x20\x20\x20\ -\x20\x20\x20\x3c\x64\x63\x3a\x74\x79\x70\x65\x0a\x20\x20\x20\x20\ -\x20\x20\x20\x20\x20\x20\x20\x72\x64\x66\x3a\x72\x65\x73\x6f\x75\ -\x72\x63\x65\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x70\x75\x72\x6c\ -\x2e\x6f\x72\x67\x2f\x64\x63\x2f\x64\x63\x6d\x69\x74\x79\x70\x65\ -\x2f\x53\x74\x69\x6c\x6c\x49\x6d\x61\x67\x65\x22\x20\x2f\x3e\x0a\ -\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x63\x3a\x74\x69\x74\x6c\ -\x65\x3e\x3c\x2f\x64\x63\x3a\x74\x69\x74\x6c\x65\x3e\x0a\x20\x20\ -\x20\x20\x20\x20\x3c\x2f\x63\x63\x3a\x57\x6f\x72\x6b\x3e\x0a\x20\ -\x20\x20\x20\x3c\x2f\x72\x64\x66\x3a\x52\x44\x46\x3e\x0a\x20\x20\ -\x3c\x2f\x6d\x65\x74\x61\x64\x61\x74\x61\x3e\x0a\x20\x20\x3c\x64\ -\x65\x66\x73\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x64\x65\x66\ -\x73\x31\x32\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x73\x6f\x64\x69\x70\ -\x6f\x64\x69\x3a\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\x0a\x20\x20\ -\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x75\x72\x72\ -\x65\x6e\x74\x2d\x6c\x61\x79\x65\x72\x3d\x22\x73\x76\x67\x38\x22\ -\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\ -\x69\x6e\x64\x6f\x77\x2d\x6d\x61\x78\x69\x6d\x69\x7a\x65\x64\x3d\ -\x22\x30\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\ -\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x79\x3d\x22\x32\x38\x31\x22\ -\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\ -\x69\x6e\x64\x6f\x77\x2d\x78\x3d\x22\x31\x34\x33\x33\x22\x0a\x20\ -\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x79\x3d\ -\x22\x31\x32\x2e\x34\x30\x39\x37\x39\x37\x22\x0a\x20\x20\x20\x20\ -\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x78\x3d\x22\x31\x32\ -\x2e\x38\x32\x38\x32\x31\x36\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\ -\x6b\x73\x63\x61\x70\x65\x3a\x7a\x6f\x6f\x6d\x3d\x22\x32\x31\x22\ -\x0a\x20\x20\x20\x20\x20\x73\x68\x6f\x77\x67\x72\x69\x64\x3d\x22\ -\x66\x61\x6c\x73\x65\x22\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\ -\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\x31\x30\x22\x0a\x20\x20\x20\ -\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\ -\x77\x2d\x68\x65\x69\x67\x68\x74\x3d\x22\x39\x35\x39\x22\x0a\x20\ -\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\ -\x64\x6f\x77\x2d\x77\x69\x64\x74\x68\x3d\x22\x31\x35\x34\x32\x22\ -\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x70\ -\x61\x67\x65\x73\x68\x61\x64\x6f\x77\x3d\x22\x32\x22\x0a\x20\x20\ -\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x70\x61\x67\x65\ -\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\x30\x22\x0a\x20\x20\x20\x20\ -\x20\x67\x75\x69\x64\x65\x74\x6f\x6c\x65\x72\x61\x6e\x63\x65\x3d\ -\x22\x31\x30\x22\x0a\x20\x20\x20\x20\x20\x67\x72\x69\x64\x74\x6f\ -\x6c\x65\x72\x61\x6e\x63\x65\x3d\x22\x31\x30\x22\x0a\x20\x20\x20\ -\x20\x20\x6f\x62\x6a\x65\x63\x74\x74\x6f\x6c\x65\x72\x61\x6e\x63\ -\x65\x3d\x22\x31\x30\x22\x0a\x20\x20\x20\x20\x20\x62\x6f\x72\x64\ -\x65\x72\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\x31\x22\x0a\x20\x20\ -\x20\x20\x20\x62\x6f\x72\x64\x65\x72\x63\x6f\x6c\x6f\x72\x3d\x22\ -\x23\x36\x36\x36\x36\x36\x36\x22\x0a\x20\x20\x20\x20\x20\x70\x61\ -\x67\x65\x63\x6f\x6c\x6f\x72\x3d\x22\x23\x66\x66\x66\x66\x66\x66\ -\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x70\x6f\x6c\x79\x67\x6f\x6e\x0a\ -\x20\x20\x20\x20\x20\x73\x74\x79\x6c\x65\x3d\x22\x66\x69\x6c\x6c\ -\x3a\x23\x66\x66\x30\x30\x30\x30\x22\x0a\x20\x20\x20\x20\x20\x69\ -\x64\x3d\x22\x70\x6f\x6c\x79\x67\x6f\x6e\x32\x22\x0a\x20\x20\x20\ -\x20\x20\x70\x6f\x69\x6e\x74\x73\x3d\x22\x37\x2e\x38\x36\x20\x32\ -\x20\x31\x36\x2e\x31\x34\x20\x32\x20\x32\x32\x20\x37\x2e\x38\x36\ -\x20\x32\x32\x20\x31\x36\x2e\x31\x34\x20\x31\x36\x2e\x31\x34\x20\ -\x32\x32\x20\x37\x2e\x38\x36\x20\x32\x32\x20\x32\x20\x31\x36\x2e\ -\x31\x34\x20\x32\x20\x37\x2e\x38\x36\x20\x37\x2e\x38\x36\x20\x32\ -\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x6c\x69\x6e\x65\x0a\x20\x20\x20\ -\x20\x20\x69\x64\x3d\x22\x6c\x69\x6e\x65\x34\x22\x0a\x20\x20\x20\ -\x20\x20\x79\x32\x3d\x22\x31\x32\x22\x0a\x20\x20\x20\x20\x20\x78\ -\x32\x3d\x22\x31\x32\x22\x0a\x20\x20\x20\x20\x20\x79\x31\x3d\x22\ -\x38\x22\x0a\x20\x20\x20\x20\x20\x78\x31\x3d\x22\x31\x32\x22\x20\ -\x2f\x3e\x0a\x20\x20\x3c\x6c\x69\x6e\x65\x0a\x20\x20\x20\x20\x20\ -\x69\x64\x3d\x22\x6c\x69\x6e\x65\x36\x22\x0a\x20\x20\x20\x20\x20\ -\x79\x32\x3d\x22\x31\x36\x22\x0a\x20\x20\x20\x20\x20\x78\x32\x3d\ -\x22\x31\x32\x2e\x30\x31\x22\x0a\x20\x20\x20\x20\x20\x79\x31\x3d\ -\x22\x31\x36\x22\x0a\x20\x20\x20\x20\x20\x78\x31\x3d\x22\x31\x32\ -\x22\x20\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\x3e\x0a\ -\x00\x00\x06\x4c\ -\x3c\ -\x73\x76\x67\x20\x77\x69\x64\x74\x68\x3d\x22\x38\x30\x22\x20\x68\ -\x65\x69\x67\x68\x74\x3d\x22\x38\x30\x22\x20\x78\x6d\x6c\x6e\x73\ -\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\ -\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x20\x66\x69\ -\x6c\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\x3e\x0a\x20\x3c\x67\x3e\x0a\ -\x20\x20\x3c\x74\x69\x74\x6c\x65\x3e\x4c\x61\x79\x65\x72\x20\x31\ -\x3c\x2f\x74\x69\x74\x6c\x65\x3e\x0a\x20\x20\x3c\x70\x61\x74\x68\ -\x20\x66\x69\x6c\x6c\x2d\x72\x75\x6c\x65\x3d\x22\x65\x76\x65\x6e\ -\x6f\x64\x64\x22\x20\x63\x6c\x69\x70\x2d\x72\x75\x6c\x65\x3d\x22\ -\x65\x76\x65\x6e\x6f\x64\x64\x22\x20\x64\x3d\x22\x6d\x33\x33\x2e\ -\x39\x32\x35\x36\x2c\x39\x2e\x36\x39\x33\x32\x34\x63\x2d\x30\x2e\ -\x37\x36\x30\x38\x2c\x30\x20\x2d\x31\x2e\x34\x38\x30\x32\x2c\x30\ -\x2e\x33\x34\x36\x34\x33\x20\x2d\x31\x2e\x39\x35\x34\x35\x2c\x30\ -\x2e\x39\x34\x31\x32\x33\x6c\x2d\x35\x2e\x33\x32\x31\x2c\x36\x2e\ -\x36\x37\x32\x33\x6c\x2d\x31\x31\x2e\x36\x35\x30\x31\x2c\x30\x63\ -\x2d\x31\x2e\x33\x38\x30\x37\x2c\x30\x20\x2d\x32\x2e\x35\x2c\x31\ -\x2e\x31\x31\x39\x33\x20\x2d\x32\x2e\x35\x2c\x32\x2e\x35\x63\x30\ -\x2c\x31\x2e\x33\x38\x30\x37\x20\x31\x2e\x31\x31\x39\x33\x2c\x32\ -\x2e\x35\x20\x32\x2e\x35\x2c\x32\x2e\x35\x6c\x31\x2e\x35\x2c\x30\ -\x6c\x30\x2c\x34\x31\x2e\x35\x63\x30\x2c\x33\x2e\x35\x38\x39\x39\ -\x20\x32\x2e\x39\x31\x30\x31\x2c\x36\x2e\x35\x20\x36\x2e\x35\x2c\ -\x36\x2e\x35\x6c\x33\x34\x2c\x30\x63\x33\x2e\x35\x38\x39\x38\x2c\ -\x30\x20\x36\x2e\x35\x2c\x2d\x32\x2e\x39\x31\x30\x31\x20\x36\x2e\ -\x35\x2c\x2d\x36\x2e\x35\x6c\x30\x2c\x2d\x34\x31\x2e\x35\x6c\x31\ -\x2e\x35\x2c\x30\x63\x31\x2e\x33\x38\x30\x37\x2c\x30\x20\x32\x2e\ -\x35\x2c\x2d\x31\x2e\x31\x31\x39\x33\x20\x32\x2e\x35\x2c\x2d\x32\ -\x2e\x35\x63\x30\x2c\x2d\x31\x2e\x33\x38\x30\x37\x20\x2d\x31\x2e\ -\x31\x31\x39\x33\x2c\x2d\x32\x2e\x35\x20\x2d\x32\x2e\x35\x2c\x2d\ -\x32\x2e\x35\x6c\x2d\x31\x31\x2e\x36\x35\x30\x31\x2c\x30\x6c\x2d\ -\x35\x2e\x33\x32\x30\x39\x2c\x2d\x36\x2e\x36\x37\x32\x32\x63\x2d\ -\x30\x2e\x34\x37\x34\x34\x2c\x2d\x30\x2e\x35\x39\x34\x39\x20\x2d\ -\x31\x2e\x31\x39\x33\x38\x2c\x2d\x30\x2e\x39\x34\x31\x33\x33\x20\ -\x2d\x31\x2e\x39\x35\x34\x36\x2c\x2d\x30\x2e\x39\x34\x31\x33\x33\ -\x6c\x2d\x31\x32\x2e\x31\x34\x38\x38\x2c\x30\x7a\x6d\x2d\x30\x2e\ -\x39\x32\x35\x36\x2c\x31\x37\x2e\x36\x31\x33\x35\x33\x63\x31\x2e\ -\x33\x38\x30\x37\x2c\x30\x20\x32\x2e\x35\x2c\x31\x2e\x31\x31\x39\ -\x33\x20\x32\x2e\x35\x2c\x32\x2e\x35\x6c\x30\x2c\x32\x38\x63\x30\ -\x2c\x31\x2e\x33\x38\x30\x37\x20\x2d\x31\x2e\x31\x31\x39\x33\x2c\ -\x32\x2e\x35\x20\x2d\x32\x2e\x35\x2c\x32\x2e\x35\x63\x2d\x31\x2e\ -\x33\x38\x30\x37\x2c\x30\x20\x2d\x32\x2e\x35\x2c\x2d\x31\x2e\x31\ -\x31\x39\x33\x20\x2d\x32\x2e\x35\x2c\x2d\x32\x2e\x35\x6c\x30\x2c\ -\x2d\x32\x38\x63\x30\x2c\x2d\x31\x2e\x33\x38\x30\x37\x20\x31\x2e\ -\x31\x31\x39\x33\x2c\x2d\x32\x2e\x35\x20\x32\x2e\x35\x2c\x2d\x32\ -\x2e\x35\x7a\x6d\x31\x36\x2e\x35\x2c\x32\x2e\x35\x63\x30\x2c\x2d\ -\x31\x2e\x33\x38\x30\x37\x20\x2d\x31\x2e\x31\x31\x39\x33\x2c\x2d\ -\x32\x2e\x35\x20\x2d\x32\x2e\x35\x2c\x2d\x32\x2e\x35\x63\x2d\x31\ -\x2e\x33\x38\x30\x37\x2c\x30\x20\x2d\x32\x2e\x35\x2c\x31\x2e\x31\ -\x31\x39\x33\x20\x2d\x32\x2e\x35\x2c\x32\x2e\x35\x6c\x30\x2c\x32\ -\x38\x63\x30\x2c\x31\x2e\x33\x38\x30\x37\x20\x31\x2e\x31\x31\x39\ -\x33\x2c\x32\x2e\x35\x20\x32\x2e\x35\x2c\x32\x2e\x35\x63\x31\x2e\ -\x33\x38\x30\x37\x2c\x30\x20\x32\x2e\x35\x2c\x2d\x31\x2e\x31\x31\ -\x39\x33\x20\x32\x2e\x35\x2c\x2d\x32\x2e\x35\x6c\x30\x2c\x2d\x32\ -\x38\x7a\x6d\x2d\x32\x2e\x35\x34\x36\x34\x2c\x2d\x31\x32\x2e\x35\ -\x30\x31\x34\x6c\x2d\x32\x2e\x30\x38\x33\x32\x2c\x2d\x32\x2e\x36\ -\x31\x32\x31\x6c\x2d\x39\x2e\x37\x34\x30\x38\x2c\x30\x6c\x2d\x32\ -\x2e\x30\x38\x33\x32\x2c\x32\x2e\x36\x31\x32\x31\x6c\x31\x33\x2e\ -\x39\x30\x37\x32\x2c\x30\x7a\x22\x20\x66\x69\x6c\x6c\x3d\x22\x23\ -\x43\x32\x43\x43\x44\x45\x22\x20\x69\x64\x3d\x22\x73\x76\x67\x5f\ -\x31\x22\x2f\x3e\x0a\x20\x20\x3c\x70\x61\x74\x68\x20\x66\x69\x6c\ -\x6c\x3d\x22\x23\x66\x66\x30\x30\x30\x30\x22\x20\x64\x3d\x22\x6d\ -\x37\x2e\x33\x33\x38\x33\x2c\x33\x39\x2e\x39\x39\x39\x39\x34\x6c\ -\x30\x2c\x30\x63\x30\x2c\x2d\x31\x38\x2e\x30\x38\x35\x33\x35\x20\ -\x31\x34\x2e\x36\x32\x33\x31\x36\x2c\x2d\x33\x32\x2e\x37\x34\x36\ -\x35\x31\x20\x33\x32\x2e\x36\x36\x31\x36\x37\x2c\x2d\x33\x32\x2e\ -\x37\x34\x36\x35\x31\x6c\x30\x2c\x30\x63\x38\x2e\x36\x36\x32\x35\ -\x33\x2c\x30\x20\x31\x36\x2e\x39\x37\x30\x32\x35\x2c\x33\x2e\x34\ -\x35\x30\x30\x38\x20\x32\x33\x2e\x30\x39\x35\x33\x32\x2c\x39\x2e\ -\x35\x39\x31\x32\x36\x63\x36\x2e\x31\x32\x35\x33\x31\x2c\x36\x2e\ -\x31\x34\x31\x31\x39\x20\x39\x2e\x35\x36\x36\x34\x32\x2c\x31\x34\ -\x2e\x34\x37\x30\x34\x34\x20\x39\x2e\x35\x36\x36\x34\x32\x2c\x32\ -\x33\x2e\x31\x35\x35\x32\x34\x6c\x30\x2c\x30\x63\x30\x2c\x31\x38\ -\x2e\x30\x38\x35\x35\x36\x20\x2d\x31\x34\x2e\x36\x32\x33\x30\x35\ -\x2c\x33\x32\x2e\x37\x34\x36\x36\x33\x20\x2d\x33\x32\x2e\x36\x36\ -\x31\x37\x33\x2c\x33\x32\x2e\x37\x34\x36\x36\x33\x6c\x30\x2c\x30\ -\x63\x2d\x31\x38\x2e\x30\x33\x38\x35\x31\x2c\x30\x20\x2d\x33\x32\ -\x2e\x36\x36\x31\x36\x37\x2c\x2d\x31\x34\x2e\x36\x36\x31\x30\x36\ -\x20\x2d\x33\x32\x2e\x36\x36\x31\x36\x37\x2c\x2d\x33\x32\x2e\x37\ -\x34\x36\x36\x33\x63\x30\x2c\x30\x20\x30\x2c\x30\x20\x30\x2c\x30\ -\x6c\x2d\x30\x2e\x30\x30\x30\x30\x31\x2c\x30\x7a\x6d\x35\x32\x2e\ -\x37\x34\x31\x32\x31\x2c\x31\x34\x2e\x36\x34\x39\x31\x36\x6c\x30\ -\x2c\x30\x63\x37\x2e\x31\x39\x31\x30\x32\x2c\x2d\x39\x2e\x39\x30\ -\x38\x33\x34\x20\x36\x2e\x31\x32\x32\x30\x39\x2c\x2d\x32\x33\x2e\ -\x35\x38\x39\x34\x38\x20\x2d\x32\x2e\x35\x32\x30\x31\x35\x2c\x2d\ -\x33\x32\x2e\x32\x35\x34\x30\x37\x63\x2d\x38\x2e\x36\x34\x32\x32\ -\x35\x2c\x2d\x38\x2e\x36\x36\x34\x36\x36\x20\x2d\x32\x32\x2e\x32\ -\x38\x37\x39\x33\x2c\x2d\x39\x2e\x37\x33\x36\x33\x37\x20\x2d\x33\ -\x32\x2e\x31\x37\x30\x33\x33\x2c\x2d\x32\x2e\x35\x32\x36\x35\x38\ -\x6c\x33\x34\x2e\x36\x39\x30\x34\x37\x2c\x33\x34\x2e\x37\x38\x30\ -\x36\x35\x6c\x30\x2e\x30\x30\x30\x30\x31\x2c\x30\x7a\x6d\x2d\x34\ -\x30\x2e\x31\x35\x38\x38\x39\x2c\x2d\x32\x39\x2e\x32\x39\x38\x30\ -\x31\x63\x2d\x37\x2e\x31\x39\x31\x30\x37\x2c\x39\x2e\x39\x30\x38\ -\x32\x38\x20\x2d\x36\x2e\x31\x32\x32\x31\x37\x2c\x32\x33\x2e\x35\ -\x38\x39\x34\x33\x20\x32\x2e\x35\x32\x30\x30\x32\x2c\x33\x32\x2e\ -\x32\x35\x33\x39\x63\x38\x2e\x36\x34\x32\x31\x37\x2c\x38\x2e\x36\ -\x36\x34\x37\x31\x20\x32\x32\x2e\x32\x38\x37\x38\x35\x2c\x39\x2e\ -\x37\x33\x36\x34\x31\x20\x33\x32\x2e\x31\x37\x30\x32\x35\x2c\x32\ -\x2e\x35\x32\x36\x37\x6c\x2d\x33\x34\x2e\x36\x39\x30\x32\x38\x2c\ -\x2d\x33\x34\x2e\x37\x38\x30\x35\x39\x6c\x30\x2c\x30\x6c\x30\x2e\ -\x30\x30\x30\x30\x31\x2c\x2d\x30\x2e\x30\x30\x30\x30\x31\x7a\x22\ -\x20\x69\x64\x3d\x22\x73\x76\x67\x5f\x33\x22\x2f\x3e\x0a\x20\x3c\ -\x2f\x67\x3e\x0a\x0a\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x09\x97\ -\x3c\ -\x73\x76\x67\x20\x66\x69\x6c\x6c\x3d\x22\x23\x30\x30\x30\x30\x30\ -\x30\x22\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\ -\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\ -\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\x30\ -\x20\x30\x20\x35\x30\x20\x35\x30\x22\x20\x77\x69\x64\x74\x68\x3d\ -\x22\x35\x30\x70\x78\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x35\ -\x30\x70\x78\x22\x3e\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\x20\ -\x32\x35\x20\x32\x20\x43\x20\x32\x30\x2e\x39\x34\x31\x34\x30\x36\ -\x20\x32\x20\x31\x38\x2e\x31\x38\x37\x35\x20\x32\x2e\x39\x36\x38\ -\x37\x35\x20\x31\x36\x2e\x34\x33\x37\x35\x20\x34\x2e\x33\x37\x35\ -\x20\x43\x20\x31\x34\x2e\x36\x38\x37\x35\x20\x35\x2e\x37\x38\x31\ -\x32\x35\x20\x31\x34\x20\x37\x2e\x35\x38\x39\x38\x34\x34\x20\x31\ -\x34\x20\x39\x2e\x30\x39\x33\x37\x35\x20\x4c\x20\x31\x34\x20\x31\ -\x34\x20\x4c\x20\x32\x34\x20\x31\x34\x20\x4c\x20\x32\x34\x20\x31\ -\x35\x20\x4c\x20\x39\x2e\x30\x39\x33\x37\x35\x20\x31\x35\x20\x43\ -\x20\x37\x2e\x32\x36\x35\x36\x32\x35\x20\x31\x35\x20\x35\x2e\x34\ -\x31\x30\x31\x35\x36\x20\x31\x35\x2e\x37\x39\x32\x39\x36\x39\x20\ -\x34\x2e\x30\x39\x33\x37\x35\x20\x31\x37\x2e\x34\x36\x38\x37\x35\ -\x20\x43\x20\x32\x2e\x37\x37\x37\x33\x34\x34\x20\x31\x39\x2e\x31\ -\x34\x34\x35\x33\x31\x20\x32\x20\x32\x31\x2e\x36\x34\x34\x35\x33\ -\x31\x20\x32\x20\x32\x35\x20\x43\x20\x32\x20\x32\x38\x2e\x33\x35\ -\x35\x34\x36\x39\x20\x32\x2e\x37\x37\x37\x33\x34\x34\x20\x33\x30\ -\x2e\x38\x35\x35\x34\x36\x39\x20\x34\x2e\x30\x39\x33\x37\x35\x20\ -\x33\x32\x2e\x35\x33\x31\x32\x35\x20\x43\x20\x35\x2e\x34\x31\x30\ -\x31\x35\x36\x20\x33\x34\x2e\x32\x30\x37\x30\x33\x31\x20\x37\x2e\ -\x32\x36\x35\x36\x32\x35\x20\x33\x35\x20\x39\x2e\x30\x39\x33\x37\ -\x35\x20\x33\x35\x20\x4c\x20\x31\x34\x20\x33\x35\x20\x4c\x20\x31\ -\x34\x20\x34\x30\x2e\x39\x30\x36\x32\x35\x20\x43\x20\x31\x34\x20\ -\x34\x32\x2e\x34\x31\x30\x31\x35\x36\x20\x31\x34\x2e\x36\x38\x37\ -\x35\x20\x34\x34\x2e\x32\x31\x38\x37\x35\x20\x31\x36\x2e\x34\x33\ -\x37\x35\x20\x34\x35\x2e\x36\x32\x35\x20\x43\x20\x31\x38\x2e\x31\ -\x38\x37\x35\x20\x34\x37\x2e\x30\x33\x31\x32\x35\x20\x32\x30\x2e\ -\x39\x34\x31\x34\x30\x36\x20\x34\x38\x20\x32\x35\x20\x34\x38\x20\ -\x43\x20\x32\x39\x2e\x30\x35\x38\x35\x39\x34\x20\x34\x38\x20\x33\ -\x31\x2e\x38\x31\x32\x35\x20\x34\x37\x2e\x30\x33\x31\x32\x35\x20\ -\x33\x33\x2e\x35\x36\x32\x35\x20\x34\x35\x2e\x36\x32\x35\x20\x43\ -\x20\x33\x35\x2e\x33\x31\x32\x35\x20\x34\x34\x2e\x32\x31\x38\x37\ -\x35\x20\x33\x36\x20\x34\x32\x2e\x34\x31\x30\x31\x35\x36\x20\x33\ -\x36\x20\x34\x30\x2e\x39\x30\x36\x32\x35\x20\x4c\x20\x33\x36\x20\ -\x33\x36\x20\x4c\x20\x32\x36\x20\x33\x36\x20\x4c\x20\x32\x36\x20\ -\x33\x35\x20\x4c\x20\x34\x30\x2e\x39\x30\x36\x32\x35\x20\x33\x35\ -\x20\x43\x20\x34\x32\x2e\x37\x33\x34\x33\x37\x35\x20\x33\x35\x20\ -\x34\x34\x2e\x35\x38\x39\x38\x34\x34\x20\x33\x34\x2e\x32\x30\x37\ -\x30\x33\x31\x20\x34\x35\x2e\x39\x30\x36\x32\x35\x20\x33\x32\x2e\ -\x35\x33\x31\x32\x35\x20\x43\x20\x34\x37\x2e\x32\x32\x32\x36\x35\ -\x36\x20\x33\x30\x2e\x38\x35\x35\x34\x36\x39\x20\x34\x38\x20\x32\ -\x38\x2e\x33\x35\x35\x34\x36\x39\x20\x34\x38\x20\x32\x35\x20\x43\ -\x20\x34\x38\x20\x32\x31\x2e\x36\x34\x34\x35\x33\x31\x20\x34\x37\ -\x2e\x32\x32\x32\x36\x35\x36\x20\x31\x39\x2e\x31\x34\x34\x35\x33\ -\x31\x20\x34\x35\x2e\x39\x30\x36\x32\x35\x20\x31\x37\x2e\x34\x36\ -\x38\x37\x35\x20\x43\x20\x34\x34\x2e\x35\x38\x39\x38\x34\x34\x20\ -\x31\x35\x2e\x37\x39\x32\x39\x36\x39\x20\x34\x32\x2e\x37\x33\x34\ -\x33\x37\x35\x20\x31\x35\x20\x34\x30\x2e\x39\x30\x36\x32\x35\x20\ -\x31\x35\x20\x4c\x20\x33\x36\x20\x31\x35\x20\x4c\x20\x33\x36\x20\ -\x39\x2e\x30\x39\x33\x37\x35\x20\x43\x20\x33\x36\x20\x37\x2e\x35\ -\x35\x30\x37\x38\x31\x20\x33\x35\x2e\x33\x31\x36\x34\x30\x36\x20\ -\x35\x2e\x37\x33\x38\x32\x38\x31\x20\x33\x33\x2e\x35\x36\x32\x35\ -\x20\x34\x2e\x33\x34\x33\x37\x35\x20\x43\x20\x33\x31\x2e\x38\x30\ -\x38\x35\x39\x34\x20\x32\x2e\x39\x34\x39\x32\x31\x39\x20\x32\x39\ -\x2e\x30\x35\x34\x36\x38\x38\x20\x32\x20\x32\x35\x20\x32\x20\x5a\ -\x20\x4d\x20\x32\x35\x20\x34\x20\x43\x20\x32\x38\x2e\x37\x34\x36\ -\x30\x39\x34\x20\x34\x20\x33\x31\x2e\x30\x31\x35\x36\x32\x35\x20\ -\x34\x2e\x38\x37\x35\x20\x33\x32\x2e\x33\x31\x32\x35\x20\x35\x2e\ -\x39\x30\x36\x32\x35\x20\x43\x20\x33\x33\x2e\x36\x30\x39\x33\x37\ -\x35\x20\x36\x2e\x39\x33\x37\x35\x20\x33\x34\x20\x38\x2e\x31\x33\ -\x36\x37\x31\x39\x20\x33\x34\x20\x39\x2e\x30\x39\x33\x37\x35\x20\ -\x4c\x20\x33\x34\x20\x32\x31\x20\x43\x20\x33\x34\x20\x32\x32\x2e\ -\x36\x35\x36\x32\x35\x20\x33\x32\x2e\x36\x35\x36\x32\x35\x20\x32\ -\x34\x20\x33\x31\x20\x32\x34\x20\x4c\x20\x31\x39\x20\x32\x34\x20\ -\x43\x20\x31\x36\x2e\x39\x34\x31\x34\x30\x36\x20\x32\x34\x20\x31\ -\x35\x2e\x31\x36\x37\x39\x36\x39\x20\x32\x35\x2e\x32\x36\x39\x35\ -\x33\x31\x20\x31\x34\x2e\x34\x30\x36\x32\x35\x20\x32\x37\x2e\x30\ -\x36\x32\x35\x20\x43\x20\x31\x34\x2e\x32\x37\x37\x33\x34\x34\x20\ -\x32\x37\x2e\x33\x35\x39\x33\x37\x35\x20\x31\x34\x2e\x31\x36\x30\ -\x31\x35\x36\x20\x32\x37\x2e\x36\x37\x35\x37\x38\x31\x20\x31\x34\ -\x2e\x30\x39\x33\x37\x35\x20\x32\x38\x20\x43\x20\x31\x34\x2e\x30\ -\x32\x37\x33\x34\x34\x20\x32\x38\x2e\x33\x32\x34\x32\x31\x39\x20\ -\x31\x34\x20\x32\x38\x2e\x36\x35\x36\x32\x35\x20\x31\x34\x20\x32\ -\x39\x20\x4c\x20\x31\x34\x20\x33\x33\x20\x4c\x20\x39\x2e\x30\x39\ -\x33\x37\x35\x20\x33\x33\x20\x43\x20\x37\x2e\x38\x32\x34\x32\x31\ -\x39\x20\x33\x33\x20\x36\x2e\x36\x34\x38\x34\x33\x38\x20\x33\x32\ -\x2e\x35\x30\x33\x39\x30\x36\x20\x35\x2e\x36\x38\x37\x35\x20\x33\ -\x31\x2e\x32\x38\x31\x32\x35\x20\x43\x20\x34\x2e\x37\x32\x36\x35\ -\x36\x33\x20\x33\x30\x2e\x30\x35\x38\x35\x39\x34\x20\x34\x20\x32\ -\x38\x2e\x30\x34\x32\x39\x36\x39\x20\x34\x20\x32\x35\x20\x43\x20\ -\x34\x20\x32\x31\x2e\x39\x35\x37\x30\x33\x31\x20\x34\x2e\x37\x32\ -\x36\x35\x36\x33\x20\x31\x39\x2e\x39\x34\x31\x34\x30\x36\x20\x35\ -\x2e\x36\x38\x37\x35\x20\x31\x38\x2e\x37\x31\x38\x37\x35\x20\x43\ -\x20\x36\x2e\x36\x34\x38\x34\x33\x38\x20\x31\x37\x2e\x34\x39\x36\ -\x30\x39\x34\x20\x37\x2e\x38\x32\x34\x32\x31\x39\x20\x31\x37\x20\ -\x39\x2e\x30\x39\x33\x37\x35\x20\x31\x37\x20\x4c\x20\x32\x36\x20\ -\x31\x37\x20\x4c\x20\x32\x36\x20\x31\x32\x20\x4c\x20\x31\x36\x20\ -\x31\x32\x20\x4c\x20\x31\x36\x20\x39\x2e\x30\x39\x33\x37\x35\x20\ -\x43\x20\x31\x36\x20\x38\x2e\x31\x39\x39\x32\x31\x39\x20\x31\x36\ -\x2e\x33\x38\x36\x37\x31\x39\x20\x36\x2e\x39\x38\x30\x34\x36\x39\ -\x20\x31\x37\x2e\x36\x38\x37\x35\x20\x35\x2e\x39\x33\x37\x35\x20\ -\x43\x20\x31\x38\x2e\x39\x38\x38\x32\x38\x31\x20\x34\x2e\x38\x39\ -\x34\x35\x33\x31\x20\x32\x31\x2e\x32\x35\x37\x38\x31\x33\x20\x34\ -\x20\x32\x35\x20\x34\x20\x5a\x20\x4d\x20\x32\x30\x20\x37\x20\x43\ -\x20\x31\x38\x2e\x38\x39\x38\x34\x33\x38\x20\x37\x20\x31\x38\x20\ -\x37\x2e\x38\x39\x38\x34\x33\x38\x20\x31\x38\x20\x39\x20\x43\x20\ -\x31\x38\x20\x31\x30\x2e\x31\x30\x31\x35\x36\x33\x20\x31\x38\x2e\ -\x38\x39\x38\x34\x33\x38\x20\x31\x31\x20\x32\x30\x20\x31\x31\x20\ -\x43\x20\x32\x31\x2e\x31\x30\x31\x35\x36\x33\x20\x31\x31\x20\x32\ -\x32\x20\x31\x30\x2e\x31\x30\x31\x35\x36\x33\x20\x32\x32\x20\x39\ -\x20\x43\x20\x32\x32\x20\x37\x2e\x38\x39\x38\x34\x33\x38\x20\x32\ -\x31\x2e\x31\x30\x31\x35\x36\x33\x20\x37\x20\x32\x30\x20\x37\x20\ -\x5a\x20\x4d\x20\x33\x36\x20\x31\x37\x20\x4c\x20\x34\x30\x2e\x39\ -\x30\x36\x32\x35\x20\x31\x37\x20\x43\x20\x34\x32\x2e\x31\x37\x35\ -\x37\x38\x31\x20\x31\x37\x20\x34\x33\x2e\x33\x35\x31\x35\x36\x33\ -\x20\x31\x37\x2e\x34\x39\x36\x30\x39\x34\x20\x34\x34\x2e\x33\x31\ -\x32\x35\x20\x31\x38\x2e\x37\x31\x38\x37\x35\x20\x43\x20\x34\x35\ -\x2e\x32\x37\x33\x34\x33\x38\x20\x31\x39\x2e\x39\x34\x31\x34\x30\ -\x36\x20\x34\x36\x20\x32\x31\x2e\x39\x35\x37\x30\x33\x31\x20\x34\ -\x36\x20\x32\x35\x20\x43\x20\x34\x36\x20\x32\x38\x2e\x30\x34\x32\ -\x39\x36\x39\x20\x34\x35\x2e\x32\x37\x33\x34\x33\x38\x20\x33\x30\ -\x2e\x30\x35\x38\x35\x39\x34\x20\x34\x34\x2e\x33\x31\x32\x35\x20\ -\x33\x31\x2e\x32\x38\x31\x32\x35\x20\x43\x20\x34\x33\x2e\x33\x35\ -\x31\x35\x36\x33\x20\x33\x32\x2e\x35\x30\x33\x39\x30\x36\x20\x34\ -\x32\x2e\x31\x37\x35\x37\x38\x31\x20\x33\x33\x20\x34\x30\x2e\x39\ -\x30\x36\x32\x35\x20\x33\x33\x20\x4c\x20\x32\x34\x20\x33\x33\x20\ -\x4c\x20\x32\x34\x20\x33\x38\x20\x4c\x20\x33\x34\x20\x33\x38\x20\ -\x4c\x20\x33\x34\x20\x34\x30\x2e\x39\x30\x36\x32\x35\x20\x43\x20\ -\x33\x34\x20\x34\x31\x2e\x38\x30\x30\x37\x38\x31\x20\x33\x33\x2e\ -\x36\x31\x33\x32\x38\x31\x20\x34\x33\x2e\x30\x31\x39\x35\x33\x31\ -\x20\x33\x32\x2e\x33\x31\x32\x35\x20\x34\x34\x2e\x30\x36\x32\x35\ -\x20\x43\x20\x33\x31\x2e\x30\x31\x31\x37\x31\x39\x20\x34\x35\x2e\ -\x31\x30\x35\x34\x36\x39\x20\x32\x38\x2e\x37\x34\x32\x31\x38\x38\ -\x20\x34\x36\x20\x32\x35\x20\x34\x36\x20\x43\x20\x32\x31\x2e\x32\ -\x35\x37\x38\x31\x33\x20\x34\x36\x20\x31\x38\x2e\x39\x38\x38\x32\ -\x38\x31\x20\x34\x35\x2e\x31\x30\x35\x34\x36\x39\x20\x31\x37\x2e\ -\x36\x38\x37\x35\x20\x34\x34\x2e\x30\x36\x32\x35\x20\x43\x20\x31\ -\x36\x2e\x33\x38\x36\x37\x31\x39\x20\x34\x33\x2e\x30\x31\x39\x35\ -\x33\x31\x20\x31\x36\x20\x34\x31\x2e\x38\x30\x30\x37\x38\x31\x20\ -\x31\x36\x20\x34\x30\x2e\x39\x30\x36\x32\x35\x20\x4c\x20\x31\x36\ -\x20\x32\x39\x20\x43\x20\x31\x36\x20\x32\x38\x2e\x37\x39\x32\x39\ -\x36\x39\x20\x31\x36\x2e\x30\x32\x33\x34\x33\x38\x20\x32\x38\x2e\ -\x36\x30\x31\x35\x36\x33\x20\x31\x36\x2e\x30\x36\x32\x35\x20\x32\ -\x38\x2e\x34\x30\x36\x32\x35\x20\x43\x20\x31\x36\x2e\x33\x34\x33\ -\x37\x35\x20\x32\x37\x2e\x30\x33\x39\x30\x36\x33\x20\x31\x37\x2e\ -\x35\x35\x30\x37\x38\x31\x20\x32\x36\x20\x31\x39\x20\x32\x36\x20\ -\x4c\x20\x33\x31\x20\x32\x36\x20\x43\x20\x33\x33\x2e\x37\x34\x36\ -\x30\x39\x34\x20\x32\x36\x20\x33\x36\x20\x32\x33\x2e\x37\x34\x36\ -\x30\x39\x34\x20\x33\x36\x20\x32\x31\x20\x5a\x20\x4d\x20\x33\x30\ -\x20\x33\x39\x20\x43\x20\x32\x38\x2e\x38\x39\x38\x34\x33\x38\x20\ -\x33\x39\x20\x32\x38\x20\x33\x39\x2e\x38\x39\x38\x34\x33\x38\x20\ -\x32\x38\x20\x34\x31\x20\x43\x20\x32\x38\x20\x34\x32\x2e\x31\x30\ -\x31\x35\x36\x33\x20\x32\x38\x2e\x38\x39\x38\x34\x33\x38\x20\x34\ -\x33\x20\x33\x30\x20\x34\x33\x20\x43\x20\x33\x31\x2e\x31\x30\x31\ -\x35\x36\x33\x20\x34\x33\x20\x33\x32\x20\x34\x32\x2e\x31\x30\x31\ -\x35\x36\x33\x20\x33\x32\x20\x34\x31\x20\x43\x20\x33\x32\x20\x33\ -\x39\x2e\x38\x39\x38\x34\x33\x38\x20\x33\x31\x2e\x31\x30\x31\x35\ -\x36\x33\x20\x33\x39\x20\x33\x30\x20\x33\x39\x20\x5a\x22\x2f\x3e\ -\x3c\x2f\x73\x76\x67\x3e\ -" - -qt_resource_name = b"\ -\x00\x09\ -\x00\x28\xbf\x23\ -\x00\x73\ -\x00\x74\x00\x79\x00\x6c\x00\x65\x00\x2e\x00\x63\x00\x73\x00\x73\ -\x00\x05\ -\x00\x6f\xa6\x53\ -\x00\x69\ -\x00\x63\x00\x6f\x00\x6e\x00\x73\ -\x00\x0f\ -\x00\x50\xd7\x47\ -\x00\x70\ -\x00\x6c\x00\x75\x00\x73\x00\x2d\x00\x73\x00\x71\x00\x75\x00\x61\x00\x72\x00\x65\x00\x2e\x00\x73\x00\x76\x00\x67\ -\x00\x0c\ -\x02\x33\x2a\x87\ -\x00\x6e\ -\x00\x6f\x00\x2d\x00\x61\x00\x6c\x00\x65\x00\x72\x00\x74\x00\x2e\x00\x73\x00\x76\x00\x67\ -\x00\x10\ -\x03\xdc\xdd\x87\ -\x00\x73\ -\x00\x74\x00\x61\x00\x72\x00\x2d\x00\x63\x00\x72\x00\x6f\x00\x73\x00\x73\x00\x65\x00\x64\x00\x2e\x00\x73\x00\x76\x00\x67\ -\x00\x08\ -\x05\x77\x54\xa7\ -\x00\x6c\ -\x00\x6f\x00\x61\x00\x64\x00\x2e\x00\x73\x00\x76\x00\x67\ -\x00\x08\ -\x05\xa8\x57\x87\ -\x00\x63\ -\x00\x6f\x00\x64\x00\x65\x00\x2e\x00\x73\x00\x76\x00\x67\ -\x00\x0a\ -\x08\x4a\xc4\x07\ -\x00\x65\ -\x00\x78\x00\x70\x00\x61\x00\x6e\x00\x64\x00\x2e\x00\x73\x00\x76\x00\x67\ -\x00\x09\ -\x08\x9b\xad\xc7\ -\x00\x74\ -\x00\x72\x00\x61\x00\x73\x00\x68\x00\x2e\x00\x73\x00\x76\x00\x67\ -\x00\x08\ -\x08\xc8\x55\xe7\ -\x00\x73\ -\x00\x61\x00\x76\x00\x65\x00\x2e\x00\x73\x00\x76\x00\x67\ -\x00\x07\ -\x09\xc7\x5a\x27\ -\x00\x73\ -\x00\x65\x00\x74\x00\x2e\x00\x73\x00\x76\x00\x67\ -\x00\x08\ -\x0a\x85\x55\x87\ -\x00\x73\ -\x00\x74\x00\x61\x00\x72\x00\x2e\x00\x73\x00\x76\x00\x67\ -\x00\x0a\ -\x0a\xc8\xf6\x87\ -\x00\x66\ -\x00\x6f\x00\x6c\x00\x64\x00\x65\x00\x72\x00\x2e\x00\x73\x00\x76\x00\x67\ -\x00\x0c\ -\x0a\xdc\x3f\xc7\ -\x00\x63\ -\x00\x6f\x00\x6c\x00\x6c\x00\x61\x00\x70\x00\x73\x00\x65\x00\x2e\x00\x73\x00\x76\x00\x67\ -\x00\x0f\ -\x0b\x14\x80\xa7\ -\x00\x67\ -\x00\x72\x00\x65\x00\x65\x00\x6e\x00\x2d\x00\x61\x00\x6c\x00\x65\x00\x72\x00\x74\x00\x2e\x00\x73\x00\x76\x00\x67\ -\x00\x0b\ -\x0c\x6a\x21\xc7\ -\x00\x72\ -\x00\x65\x00\x66\x00\x72\x00\x65\x00\x73\x00\x68\x00\x2e\x00\x73\x00\x76\x00\x67\ -\x00\x0a\ -\x0c\xad\x02\x87\ -\x00\x64\ -\x00\x65\x00\x6c\x00\x65\x00\x74\x00\x65\x00\x2e\x00\x73\x00\x76\x00\x67\ -\x00\x0d\ -\x0d\xf9\x2b\x67\ -\x00\x72\ -\x00\x65\x00\x64\x00\x2d\x00\x61\x00\x6c\x00\x65\x00\x72\x00\x74\x00\x2e\x00\x73\x00\x76\x00\x67\ -\x00\x11\ -\x0e\x2c\x55\xe7\ -\x00\x74\ -\x00\x72\x00\x61\x00\x73\x00\x68\x00\x2d\x00\x63\x00\x72\x00\x6f\x00\x73\x00\x73\x00\x65\x00\x64\x00\x2e\x00\x73\x00\x76\x00\x67\ -\ -\x00\x0a\ -\x0f\x6e\x5b\x87\ -\x00\x70\ -\x00\x79\x00\x74\x00\x68\x00\x6f\x00\x6e\x00\x2e\x00\x73\x00\x76\x00\x67\ -" - -qt_resource_struct_v1 = b"\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x00\x18\x00\x02\x00\x00\x00\x12\x00\x00\x00\x03\ -\x00\x00\x00\x28\x00\x00\x00\x00\x00\x01\x00\x00\x00\x04\ -\x00\x00\x00\x4c\x00\x00\x00\x00\x00\x01\x00\x00\x01\x7d\ -\x00\x00\x00\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x03\x21\ -\x00\x00\x00\x90\x00\x00\x00\x00\x00\x01\x00\x00\x0a\xfe\ -\x00\x00\x00\xa6\x00\x00\x00\x00\x00\x01\x00\x00\x0c\x6e\ -\x00\x00\x00\xbc\x00\x01\x00\x00\x00\x01\x00\x00\x0d\xa5\ -\x00\x00\x00\xd6\x00\x00\x00\x00\x00\x01\x00\x00\x11\x51\ -\x00\x00\x00\xee\x00\x00\x00\x00\x00\x01\x00\x00\x14\xf0\ -\x00\x00\x01\x04\x00\x00\x00\x00\x00\x01\x00\x00\x16\x7c\ -\x00\x00\x01\x18\x00\x00\x00\x00\x00\x01\x00\x00\x17\xf0\ -\x00\x00\x01\x2e\x00\x00\x00\x00\x00\x01\x00\x00\x1d\xae\ -\x00\x00\x01\x48\x00\x00\x00\x00\x00\x01\x00\x00\x22\x74\ -\x00\x00\x01\x66\x00\x00\x00\x00\x00\x01\x00\x00\x2c\x07\ -\x00\x00\x01\x8a\x00\x00\x00\x00\x00\x01\x00\x00\x33\xa8\ -\x00\x00\x01\xa6\x00\x00\x00\x00\x00\x01\x00\x00\x35\x3c\ -\x00\x00\x01\xc0\x00\x00\x00\x00\x00\x01\x00\x00\x36\xb6\ -\x00\x00\x01\xe0\x00\x00\x00\x00\x00\x01\x00\x00\x3e\x57\ -\x00\x00\x02\x08\x00\x00\x00\x00\x00\x01\x00\x00\x44\xa7\ -" - -qt_resource_struct_v2 = b"\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd5\ -\x00\x00\x00\x18\x00\x02\x00\x00\x00\x12\x00\x00\x00\x03\ -\x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x28\x00\x00\x00\x00\x00\x01\x00\x00\x00\x04\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd4\ -\x00\x00\x00\x4c\x00\x00\x00\x00\x00\x01\x00\x00\x01\x7d\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd3\ -\x00\x00\x00\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x03\x21\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd4\ -\x00\x00\x00\x90\x00\x00\x00\x00\x00\x01\x00\x00\x0a\xfe\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd4\ -\x00\x00\x00\xa6\x00\x00\x00\x00\x00\x01\x00\x00\x0c\x6e\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd3\ -\x00\x00\x00\xbc\x00\x01\x00\x00\x00\x01\x00\x00\x0d\xa5\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd3\ -\x00\x00\x00\xd6\x00\x00\x00\x00\x00\x01\x00\x00\x11\x51\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd5\ -\x00\x00\x00\xee\x00\x00\x00\x00\x00\x01\x00\x00\x14\xf0\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd4\ -\x00\x00\x01\x04\x00\x00\x00\x00\x00\x01\x00\x00\x16\x7c\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd4\ -\x00\x00\x01\x18\x00\x00\x00\x00\x00\x01\x00\x00\x17\xf0\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd5\ -\x00\x00\x01\x2e\x00\x00\x00\x00\x00\x01\x00\x00\x1d\xae\ -\x00\x00\x01\x9a\x4b\xc3\x1d\x94\ -\x00\x00\x01\x48\x00\x00\x00\x00\x00\x01\x00\x00\x22\x74\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd3\ -\x00\x00\x01\x66\x00\x00\x00\x00\x00\x01\x00\x00\x2c\x07\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd2\ -\x00\x00\x01\x8a\x00\x00\x00\x00\x00\x01\x00\x00\x33\xa8\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd4\ -\x00\x00\x01\xa6\x00\x00\x00\x00\x00\x01\x00\x00\x35\x3c\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd3\ -\x00\x00\x01\xc0\x00\x00\x00\x00\x00\x01\x00\x00\x36\xb6\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd3\ -\x00\x00\x01\xe0\x00\x00\x00\x00\x00\x01\x00\x00\x3e\x57\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd5\ -\x00\x00\x02\x08\x00\x00\x00\x00\x00\x01\x00\x00\x44\xa7\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd4\ -" - -qt_version = [int(v) for v in QtCore.qVersion().split('.')] -if qt_version < [5, 8, 0]: - rcc_version = 1 - qt_resource_struct = qt_resource_struct_v1 -else: - rcc_version = 2 - qt_resource_struct = qt_resource_struct_v2 - -def qInitResources(): - QtCore.qRegisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) - -def qCleanupResources(): - QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) - -qInitResources() +# -*- coding: utf-8 -*- + +# Resource object code +# +# Created by: The Resource Compiler for PyQt5 (Qt v5.15.2) +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore + +qt_resource_data = b"\ +\x00\x00\x00\x00\ +\ +\x00\x00\x01\x75\ +\x3c\ +\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ +\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ +\x30\x2f\x73\x76\x67\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x32\x34\ +\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x32\x34\x22\x20\x76\x69\ +\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\x30\x20\x32\x34\x20\x32\x34\ +\x22\x20\x66\x69\x6c\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\x20\x73\x74\ +\x72\x6f\x6b\x65\x3d\x22\x63\x75\x72\x72\x65\x6e\x74\x43\x6f\x6c\ +\x6f\x72\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\x68\ +\x3d\x22\x32\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\ +\x63\x61\x70\x3d\x22\x72\x6f\x75\x6e\x64\x22\x20\x73\x74\x72\x6f\ +\x6b\x65\x2d\x6c\x69\x6e\x65\x6a\x6f\x69\x6e\x3d\x22\x72\x6f\x75\ +\x6e\x64\x22\x20\x63\x6c\x61\x73\x73\x3d\x22\x66\x65\x61\x74\x68\ +\x65\x72\x20\x66\x65\x61\x74\x68\x65\x72\x2d\x70\x6c\x75\x73\x2d\ +\x73\x71\x75\x61\x72\x65\x22\x3e\x3c\x72\x65\x63\x74\x20\x78\x3d\ +\x22\x33\x22\x20\x79\x3d\x22\x33\x22\x20\x77\x69\x64\x74\x68\x3d\ +\x22\x31\x38\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x31\x38\x22\ +\x20\x72\x78\x3d\x22\x32\x22\x20\x72\x79\x3d\x22\x32\x22\x3e\x3c\ +\x2f\x72\x65\x63\x74\x3e\x3c\x6c\x69\x6e\x65\x20\x78\x31\x3d\x22\ +\x31\x32\x22\x20\x79\x31\x3d\x22\x38\x22\x20\x78\x32\x3d\x22\x31\ +\x32\x22\x20\x79\x32\x3d\x22\x31\x36\x22\x3e\x3c\x2f\x6c\x69\x6e\ +\x65\x3e\x3c\x6c\x69\x6e\x65\x20\x78\x31\x3d\x22\x38\x22\x20\x79\ +\x31\x3d\x22\x31\x32\x22\x20\x78\x32\x3d\x22\x31\x36\x22\x20\x79\ +\x32\x3d\x22\x31\x32\x22\x3e\x3c\x2f\x6c\x69\x6e\x65\x3e\x3c\x2f\ +\x73\x76\x67\x3e\ +\x00\x00\x01\xa0\ +\x3c\ +\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ +\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ +\x30\x2f\x73\x76\x67\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x32\x34\ +\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x32\x34\x22\x20\x76\x69\ +\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\x30\x20\x32\x34\x20\x32\x34\ +\x22\x20\x66\x69\x6c\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\x20\x73\x74\ +\x72\x6f\x6b\x65\x3d\x22\x63\x75\x72\x72\x65\x6e\x74\x43\x6f\x6c\ +\x6f\x72\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\x68\ +\x3d\x22\x32\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\ +\x63\x61\x70\x3d\x22\x72\x6f\x75\x6e\x64\x22\x20\x73\x74\x72\x6f\ +\x6b\x65\x2d\x6c\x69\x6e\x65\x6a\x6f\x69\x6e\x3d\x22\x72\x6f\x75\ +\x6e\x64\x22\x20\x63\x6c\x61\x73\x73\x3d\x22\x66\x65\x61\x74\x68\ +\x65\x72\x20\x66\x65\x61\x74\x68\x65\x72\x2d\x61\x6c\x65\x72\x74\ +\x2d\x6f\x63\x74\x61\x67\x6f\x6e\x22\x3e\x3c\x70\x6f\x6c\x79\x67\ +\x6f\x6e\x20\x70\x6f\x69\x6e\x74\x73\x3d\x22\x37\x2e\x38\x36\x20\ +\x32\x20\x31\x36\x2e\x31\x34\x20\x32\x20\x32\x32\x20\x37\x2e\x38\ +\x36\x20\x32\x32\x20\x31\x36\x2e\x31\x34\x20\x31\x36\x2e\x31\x34\ +\x20\x32\x32\x20\x37\x2e\x38\x36\x20\x32\x32\x20\x32\x20\x31\x36\ +\x2e\x31\x34\x20\x32\x20\x37\x2e\x38\x36\x20\x37\x2e\x38\x36\x20\ +\x32\x22\x3e\x3c\x2f\x70\x6f\x6c\x79\x67\x6f\x6e\x3e\x3c\x6c\x69\ +\x6e\x65\x20\x78\x31\x3d\x22\x31\x32\x22\x20\x79\x31\x3d\x22\x38\ +\x22\x20\x78\x32\x3d\x22\x31\x32\x22\x20\x79\x32\x3d\x22\x31\x32\ +\x22\x3e\x3c\x2f\x6c\x69\x6e\x65\x3e\x3c\x6c\x69\x6e\x65\x20\x78\ +\x31\x3d\x22\x31\x32\x22\x20\x79\x31\x3d\x22\x31\x36\x22\x20\x78\ +\x32\x3d\x22\x31\x32\x2e\x30\x31\x22\x20\x79\x32\x3d\x22\x31\x36\ +\x22\x3e\x3c\x2f\x6c\x69\x6e\x65\x3e\x3c\x2f\x73\x76\x67\x3e\ +\x00\x00\x07\xd9\ +\x3c\ +\x73\x76\x67\x20\x77\x69\x64\x74\x68\x3d\x22\x38\x30\x22\x20\x68\ +\x65\x69\x67\x68\x74\x3d\x22\x38\x30\x22\x20\x78\x6d\x6c\x6e\x73\ +\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\ +\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x20\x66\x69\ +\x6c\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\x3e\x0a\x20\x3c\x67\x3e\x0a\ +\x20\x20\x3c\x74\x69\x74\x6c\x65\x3e\x4c\x61\x79\x65\x72\x20\x31\ +\x3c\x2f\x74\x69\x74\x6c\x65\x3e\x0a\x20\x20\x3c\x70\x61\x74\x68\ +\x20\x64\x3d\x22\x6d\x33\x38\x2e\x31\x34\x34\x31\x2c\x31\x35\x2e\ +\x31\x36\x31\x36\x31\x63\x30\x2e\x36\x37\x31\x38\x2c\x2d\x31\x2e\ +\x36\x37\x32\x39\x20\x33\x2e\x30\x34\x2c\x2d\x31\x2e\x36\x37\x32\ +\x39\x20\x33\x2e\x37\x31\x31\x38\x2c\x30\x6c\x35\x2e\x39\x35\x32\ +\x32\x2c\x31\x34\x2e\x38\x32\x32\x32\x63\x30\x2e\x32\x38\x36\x31\ +\x2c\x30\x2e\x37\x31\x32\x34\x20\x30\x2e\x39\x35\x34\x37\x2c\x31\ +\x2e\x31\x39\x38\x31\x20\x31\x2e\x37\x32\x30\x37\x2c\x31\x2e\x32\ +\x35\x30\x31\x6c\x31\x35\x2e\x39\x33\x36\x2c\x31\x2e\x30\x38\x30\ +\x35\x63\x31\x2e\x37\x39\x38\x37\x2c\x30\x2e\x31\x32\x32\x20\x32\ +\x2e\x35\x33\x30\x35\x2c\x32\x2e\x33\x37\x34\x34\x20\x31\x2e\x31\ +\x34\x37\x2c\x33\x2e\x35\x33\x30\x32\x6c\x2d\x31\x32\x2e\x32\x35\ +\x37\x34\x2c\x31\x30\x2e\x32\x34\x31\x32\x63\x2d\x30\x2e\x35\x38\ +\x39\x31\x2c\x30\x2e\x34\x39\x32\x32\x20\x2d\x30\x2e\x38\x34\x34\ +\x35\x2c\x31\x2e\x32\x37\x38\x32\x20\x2d\x30\x2e\x36\x35\x37\x32\ +\x2c\x32\x2e\x30\x32\x32\x37\x6c\x33\x2e\x38\x39\x36\x39\x2c\x31\ +\x35\x2e\x34\x39\x63\x30\x2e\x34\x33\x39\x38\x2c\x31\x2e\x37\x34\ +\x38\x33\x20\x2d\x31\x2e\x34\x37\x36\x32\x2c\x33\x2e\x31\x34\x30\ +\x34\x20\x2d\x33\x2e\x30\x30\x33\x2c\x32\x2e\x31\x38\x31\x38\x6c\ +\x2d\x31\x33\x2e\x35\x32\x37\x37\x2c\x2d\x38\x2e\x34\x39\x32\x38\ +\x63\x2d\x30\x2e\x36\x35\x30\x32\x2c\x2d\x30\x2e\x34\x30\x38\x32\ +\x20\x2d\x31\x2e\x34\x37\x36\x36\x2c\x2d\x30\x2e\x34\x30\x38\x32\ +\x20\x2d\x32\x2e\x31\x32\x36\x38\x2c\x30\x6c\x2d\x31\x33\x2e\x35\ +\x32\x37\x37\x2c\x38\x2e\x34\x39\x32\x38\x63\x2d\x31\x2e\x35\x32\ +\x36\x38\x2c\x30\x2e\x39\x35\x38\x36\x20\x2d\x33\x2e\x34\x34\x32\ +\x38\x2c\x2d\x30\x2e\x34\x33\x33\x35\x20\x2d\x33\x2e\x30\x30\x33\ +\x2c\x2d\x32\x2e\x31\x38\x31\x38\x6c\x33\x2e\x38\x39\x36\x39\x2c\ +\x2d\x31\x35\x2e\x34\x39\x63\x30\x2e\x31\x38\x37\x33\x2c\x2d\x30\ +\x2e\x37\x34\x34\x35\x20\x2d\x30\x2e\x30\x36\x38\x31\x2c\x2d\x31\ +\x2e\x35\x33\x30\x35\x20\x2d\x30\x2e\x36\x35\x37\x32\x2c\x2d\x32\ +\x2e\x30\x32\x32\x37\x6c\x2d\x31\x32\x2e\x32\x35\x37\x34\x2c\x2d\ +\x31\x30\x2e\x32\x34\x31\x32\x63\x2d\x31\x2e\x33\x38\x33\x35\x2c\ +\x2d\x31\x2e\x31\x35\x35\x38\x20\x2d\x30\x2e\x36\x35\x31\x37\x2c\ +\x2d\x33\x2e\x34\x30\x38\x32\x20\x31\x2e\x31\x34\x37\x2c\x2d\x33\ +\x2e\x35\x33\x30\x32\x6c\x31\x35\x2e\x39\x33\x36\x2c\x2d\x31\x2e\ +\x30\x38\x30\x35\x63\x30\x2e\x37\x36\x36\x2c\x2d\x30\x2e\x30\x35\ +\x32\x20\x31\x2e\x34\x33\x34\x36\x2c\x2d\x30\x2e\x35\x33\x37\x37\ +\x20\x31\x2e\x37\x32\x30\x37\x2c\x2d\x31\x2e\x32\x35\x30\x31\x6c\ +\x35\x2e\x39\x35\x32\x32\x2c\x2d\x31\x34\x2e\x38\x32\x32\x32\x7a\ +\x22\x20\x66\x69\x6c\x6c\x3d\x22\x23\x46\x32\x39\x39\x34\x41\x22\ +\x20\x69\x64\x3d\x22\x73\x76\x67\x5f\x31\x22\x2f\x3e\x0a\x20\x20\ +\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x6d\x33\x39\x2e\x30\x35\x31\ +\x35\x2c\x32\x36\x2e\x33\x31\x30\x38\x63\x30\x2e\x33\x33\x35\x39\ +\x2c\x2d\x30\x2e\x38\x33\x36\x34\x20\x31\x2e\x35\x32\x30\x31\x2c\ +\x2d\x30\x2e\x38\x33\x36\x34\x20\x31\x2e\x38\x35\x36\x2c\x30\x6c\ +\x32\x2e\x39\x37\x36\x2c\x37\x2e\x34\x31\x31\x31\x63\x30\x2e\x31\ +\x34\x33\x31\x2c\x30\x2e\x33\x35\x36\x32\x20\x30\x2e\x34\x37\x37\ +\x34\x2c\x30\x2e\x35\x39\x39\x31\x20\x30\x2e\x38\x36\x30\x34\x2c\ +\x30\x2e\x36\x32\x35\x31\x6c\x37\x2e\x39\x36\x38\x2c\x30\x2e\x35\ +\x34\x30\x33\x63\x30\x2e\x38\x39\x39\x33\x2c\x30\x2e\x30\x36\x30\ +\x39\x20\x31\x2e\x32\x36\x35\x32\x2c\x31\x2e\x31\x38\x37\x31\x20\ +\x30\x2e\x35\x37\x33\x35\x2c\x31\x2e\x37\x36\x35\x31\x6c\x2d\x36\ +\x2e\x31\x32\x38\x37\x2c\x35\x2e\x31\x32\x30\x35\x63\x2d\x30\x2e\ +\x32\x39\x34\x36\x2c\x30\x2e\x32\x34\x36\x32\x20\x2d\x30\x2e\x34\ +\x32\x32\x33\x2c\x30\x2e\x36\x33\x39\x32\x20\x2d\x30\x2e\x33\x32\ +\x38\x36\x2c\x31\x2e\x30\x31\x31\x34\x6c\x31\x2e\x39\x34\x38\x34\ +\x2c\x37\x2e\x37\x34\x35\x63\x30\x2e\x32\x31\x39\x39\x2c\x30\x2e\ +\x38\x37\x34\x32\x20\x2d\x30\x2e\x37\x33\x38\x31\x2c\x31\x2e\x35\ +\x37\x30\x32\x20\x2d\x31\x2e\x35\x30\x31\x35\x2c\x31\x2e\x30\x39\ +\x30\x39\x6c\x2d\x36\x2e\x37\x36\x33\x38\x2c\x2d\x34\x2e\x32\x34\ +\x36\x34\x63\x2d\x30\x2e\x33\x32\x35\x31\x2c\x2d\x30\x2e\x32\x30\ +\x34\x31\x20\x2d\x30\x2e\x37\x33\x38\x33\x2c\x2d\x30\x2e\x32\x30\ +\x34\x31\x20\x2d\x31\x2e\x30\x36\x33\x34\x2c\x30\x6c\x2d\x36\x2e\ +\x37\x36\x33\x38\x2c\x34\x2e\x32\x34\x36\x34\x63\x2d\x30\x2e\x37\ +\x36\x33\x35\x2c\x30\x2e\x34\x37\x39\x33\x20\x2d\x31\x2e\x37\x32\ +\x31\x34\x2c\x2d\x30\x2e\x32\x31\x36\x37\x20\x2d\x31\x2e\x35\x30\ +\x31\x35\x2c\x2d\x31\x2e\x30\x39\x30\x39\x6c\x31\x2e\x39\x34\x38\ +\x34\x2c\x2d\x37\x2e\x37\x34\x35\x63\x30\x2e\x30\x39\x33\x36\x2c\ +\x2d\x30\x2e\x33\x37\x32\x32\x20\x2d\x30\x2e\x30\x33\x34\x31\x2c\ +\x2d\x30\x2e\x37\x36\x35\x32\x20\x2d\x30\x2e\x33\x32\x38\x36\x2c\ +\x2d\x31\x2e\x30\x31\x31\x34\x6c\x2d\x36\x2e\x31\x32\x38\x37\x2c\ +\x2d\x35\x2e\x31\x32\x30\x35\x63\x2d\x30\x2e\x36\x39\x31\x38\x2c\ +\x2d\x30\x2e\x35\x37\x38\x20\x2d\x30\x2e\x33\x32\x35\x38\x2c\x2d\ +\x31\x2e\x37\x30\x34\x32\x20\x30\x2e\x35\x37\x33\x35\x2c\x2d\x31\ +\x2e\x37\x36\x35\x31\x6c\x37\x2e\x39\x36\x38\x2c\x2d\x30\x2e\x35\ +\x34\x30\x33\x63\x30\x2e\x33\x38\x33\x2c\x2d\x30\x2e\x30\x32\x36\ +\x20\x30\x2e\x37\x31\x37\x33\x2c\x2d\x30\x2e\x32\x36\x38\x39\x20\ +\x30\x2e\x38\x36\x30\x33\x2c\x2d\x30\x2e\x36\x32\x35\x31\x6c\x32\ +\x2e\x39\x37\x36\x31\x2c\x2d\x37\x2e\x34\x31\x31\x31\x7a\x22\x20\ +\x66\x69\x6c\x6c\x3d\x22\x23\x46\x32\x43\x39\x34\x43\x22\x20\x69\ +\x64\x3d\x22\x73\x76\x67\x5f\x32\x22\x2f\x3e\x0a\x20\x20\x3c\x70\ +\x61\x74\x68\x20\x66\x69\x6c\x6c\x3d\x22\x23\x66\x66\x30\x30\x30\ +\x30\x22\x20\x64\x3d\x22\x6d\x35\x2c\x33\x39\x2e\x39\x39\x39\x39\ +\x34\x6c\x30\x2c\x30\x63\x30\x2c\x2d\x31\x38\x2e\x31\x36\x32\x36\ +\x38\x20\x31\x35\x2e\x36\x37\x30\x30\x35\x2c\x2d\x33\x32\x2e\x38\ +\x38\x36\x35\x34\x20\x33\x34\x2e\x39\x39\x39\x39\x37\x2c\x2d\x33\ +\x32\x2e\x38\x38\x36\x35\x34\x6c\x30\x2c\x30\x63\x39\x2e\x32\x38\ +\x32\x37\x2c\x30\x20\x31\x38\x2e\x31\x38\x35\x31\x37\x2c\x33\x2e\ +\x34\x36\x34\x38\x33\x20\x32\x34\x2e\x37\x34\x38\x37\x34\x2c\x39\ +\x2e\x36\x33\x32\x32\x38\x63\x36\x2e\x35\x36\x33\x38\x33\x2c\x36\ +\x2e\x31\x36\x37\x34\x35\x20\x31\x30\x2e\x32\x35\x31\x32\x39\x2c\ +\x31\x34\x2e\x35\x33\x32\x33\x32\x20\x31\x30\x2e\x32\x35\x31\x32\ +\x39\x2c\x32\x33\x2e\x32\x35\x34\x32\x36\x6c\x30\x2c\x30\x63\x30\ +\x2c\x31\x38\x2e\x31\x36\x32\x39\x20\x2d\x31\x35\x2e\x36\x36\x39\ +\x39\x34\x2c\x33\x32\x2e\x38\x38\x36\x36\x36\x20\x2d\x33\x35\x2e\ +\x30\x30\x30\x30\x33\x2c\x33\x32\x2e\x38\x38\x36\x36\x36\x6c\x30\ +\x2c\x30\x63\x2d\x31\x39\x2e\x33\x32\x39\x39\x32\x2c\x30\x20\x2d\ +\x33\x34\x2e\x39\x39\x39\x39\x37\x2c\x2d\x31\x34\x2e\x37\x32\x33\ +\x37\x36\x20\x2d\x33\x34\x2e\x39\x39\x39\x39\x37\x2c\x2d\x33\x32\ +\x2e\x38\x38\x36\x36\x36\x6c\x30\x2c\x30\x7a\x6d\x35\x36\x2e\x35\ +\x31\x37\x30\x33\x2c\x31\x34\x2e\x37\x31\x31\x38\x6c\x30\x2c\x30\ +\x63\x37\x2e\x37\x30\x35\x38\x34\x2c\x2d\x39\x2e\x39\x35\x30\x37\ +\x31\x20\x36\x2e\x35\x36\x30\x33\x39\x2c\x2d\x32\x33\x2e\x36\x39\ +\x30\x33\x36\x20\x2d\x32\x2e\x37\x30\x30\x35\x37\x2c\x2d\x33\x32\ +\x2e\x33\x39\x32\x63\x2d\x39\x2e\x32\x36\x30\x39\x36\x2c\x2d\x38\ +\x2e\x37\x30\x31\x37\x31\x20\x2d\x32\x33\x2e\x38\x38\x33\x35\x35\ +\x2c\x2d\x39\x2e\x37\x37\x38\x20\x2d\x33\x34\x2e\x34\x37\x33\x34\ +\x35\x2c\x2d\x32\x2e\x35\x33\x37\x33\x38\x6c\x33\x37\x2e\x31\x37\ +\x34\x30\x32\x2c\x33\x34\x2e\x39\x32\x39\x33\x38\x7a\x6d\x2d\x34\ +\x33\x2e\x30\x33\x33\x39\x33\x2c\x2d\x32\x39\x2e\x34\x32\x33\x33\ +\x63\x2d\x37\x2e\x37\x30\x35\x39\x2c\x39\x2e\x39\x35\x30\x36\x36\ +\x20\x2d\x36\x2e\x35\x36\x30\x34\x37\x2c\x32\x33\x2e\x36\x39\x30\ +\x33\x20\x32\x2e\x37\x30\x30\x34\x34\x2c\x33\x32\x2e\x33\x39\x31\ +\x38\x32\x63\x39\x2e\x32\x36\x30\x38\x38\x2c\x38\x2e\x37\x30\x31\ +\x37\x36\x20\x32\x33\x2e\x38\x38\x33\x34\x37\x2c\x39\x2e\x37\x37\ +\x38\x30\x35\x20\x33\x34\x2e\x34\x37\x33\x33\x37\x2c\x32\x2e\x35\ +\x33\x37\x35\x6c\x2d\x33\x37\x2e\x31\x37\x33\x38\x31\x2c\x2d\x33\ +\x34\x2e\x39\x32\x39\x33\x32\x6c\x30\x2c\x30\x7a\x22\x20\x69\x64\ +\x3d\x22\x73\x76\x67\x5f\x34\x22\x2f\x3e\x0a\x20\x3c\x2f\x67\x3e\ +\x0a\x0a\x3c\x2f\x73\x76\x67\x3e\ +\x00\x00\x01\x6c\ +\x3c\ +\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ +\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ +\x30\x2f\x73\x76\x67\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x32\x34\ +\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x32\x34\x22\x20\x76\x69\ +\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\x30\x20\x32\x34\x20\x32\x34\ +\x22\x20\x66\x69\x6c\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\x20\x73\x74\ +\x72\x6f\x6b\x65\x3d\x22\x63\x75\x72\x72\x65\x6e\x74\x43\x6f\x6c\ +\x6f\x72\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\x68\ +\x3d\x22\x32\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\ +\x63\x61\x70\x3d\x22\x72\x6f\x75\x6e\x64\x22\x20\x73\x74\x72\x6f\ +\x6b\x65\x2d\x6c\x69\x6e\x65\x6a\x6f\x69\x6e\x3d\x22\x72\x6f\x75\ +\x6e\x64\x22\x20\x63\x6c\x61\x73\x73\x3d\x22\x66\x65\x61\x74\x68\ +\x65\x72\x20\x66\x65\x61\x74\x68\x65\x72\x2d\x73\x68\x61\x72\x65\ +\x22\x3e\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\x34\x20\x31\x32\ +\x76\x38\x61\x32\x20\x32\x20\x30\x20\x30\x20\x30\x20\x32\x20\x32\ +\x68\x31\x32\x61\x32\x20\x32\x20\x30\x20\x30\x20\x30\x20\x32\x2d\ +\x32\x76\x2d\x38\x22\x3e\x3c\x2f\x70\x61\x74\x68\x3e\x3c\x70\x6f\ +\x6c\x79\x6c\x69\x6e\x65\x20\x70\x6f\x69\x6e\x74\x73\x3d\x22\x31\ +\x36\x20\x36\x20\x31\x32\x20\x32\x20\x38\x20\x36\x22\x3e\x3c\x2f\ +\x70\x6f\x6c\x79\x6c\x69\x6e\x65\x3e\x3c\x6c\x69\x6e\x65\x20\x78\ +\x31\x3d\x22\x31\x32\x22\x20\x79\x31\x3d\x22\x32\x22\x20\x78\x32\ +\x3d\x22\x31\x32\x22\x20\x79\x32\x3d\x22\x31\x35\x22\x3e\x3c\x2f\ +\x6c\x69\x6e\x65\x3e\x3c\x2f\x73\x76\x67\x3e\ +\x00\x00\x01\x33\ +\x3c\ +\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ +\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ +\x30\x2f\x73\x76\x67\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x32\x34\ +\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x32\x34\x22\x20\x76\x69\ +\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\x30\x20\x32\x34\x20\x32\x34\ +\x22\x20\x66\x69\x6c\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\x20\x73\x74\ +\x72\x6f\x6b\x65\x3d\x22\x63\x75\x72\x72\x65\x6e\x74\x43\x6f\x6c\ +\x6f\x72\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\x68\ +\x3d\x22\x32\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\ +\x63\x61\x70\x3d\x22\x72\x6f\x75\x6e\x64\x22\x20\x73\x74\x72\x6f\ +\x6b\x65\x2d\x6c\x69\x6e\x65\x6a\x6f\x69\x6e\x3d\x22\x72\x6f\x75\ +\x6e\x64\x22\x20\x63\x6c\x61\x73\x73\x3d\x22\x66\x65\x61\x74\x68\ +\x65\x72\x20\x66\x65\x61\x74\x68\x65\x72\x2d\x63\x6f\x64\x65\x22\ +\x3e\x3c\x70\x6f\x6c\x79\x6c\x69\x6e\x65\x20\x70\x6f\x69\x6e\x74\ +\x73\x3d\x22\x31\x36\x20\x31\x38\x20\x32\x32\x20\x31\x32\x20\x31\ +\x36\x20\x36\x22\x3e\x3c\x2f\x70\x6f\x6c\x79\x6c\x69\x6e\x65\x3e\ +\x3c\x70\x6f\x6c\x79\x6c\x69\x6e\x65\x20\x70\x6f\x69\x6e\x74\x73\ +\x3d\x22\x38\x20\x36\x20\x32\x20\x31\x32\x20\x38\x20\x31\x38\x22\ +\x3e\x3c\x2f\x70\x6f\x6c\x79\x6c\x69\x6e\x65\x3e\x3c\x2f\x73\x76\ +\x67\x3e\ +\x00\x00\x03\xa8\ +\x00\ +\x00\x0c\x99\x78\x9c\xdd\x56\x4b\x6f\xe3\x36\x10\xbe\xe7\x57\x08\ +\xca\xa5\x45\x23\x8a\xd4\xcb\xa2\x62\x79\x81\x36\x58\xb4\x87\x5e\ +\xba\xbb\xe8\x99\x21\x69\x5b\x1b\x89\x34\x28\x3a\xb6\xf7\xd7\xef\ +\x50\x0f\x5b\x76\x9c\xf4\x71\x28\xd0\x08\x36\xec\x79\x71\x66\xbe\ +\xf9\x38\xf6\xfc\xc3\xbe\xa9\xbd\x67\x69\xda\x4a\xab\xd2\x27\x08\ +\xfb\x9e\x54\x5c\x8b\x4a\xad\x4a\xff\xcb\xe7\x8f\x41\xee\x7b\xad\ +\x65\x4a\xb0\x5a\x2b\x59\xfa\x4a\xfb\x1f\x16\x37\xf3\xf6\x79\x75\ +\xe3\x79\x1e\x04\xab\xb6\x10\xbc\xf4\xd7\xd6\x6e\x8a\x30\xdc\x6c\ +\x4d\x8d\xb4\x59\x85\x82\x87\xb2\x96\x8d\x54\xb6\x0d\x09\x22\xa1\ +\x7f\x72\xe7\x27\x77\x6e\x24\xb3\xd5\xb3\xe4\xba\x69\xb4\x6a\xbb\ +\x48\xd5\xde\x4e\x9c\x8d\x58\x1e\xbd\x77\xbb\x1d\xda\xc5\x9d\x13\ +\xa1\x94\x86\x38\x0a\xa3\x28\x00\x8f\xa0\x3d\x28\xcb\xf6\xc1\x79\ +\x28\xd4\x78\x2d\x34\xc2\x18\x87\x60\x3b\x79\xfe\x3d\xaf\xa2\x05\ +\x54\x36\xf0\x3e\xba\x8f\x0a\xd4\xea\xad\xe1\x72\x09\x71\x12\x29\ +\x69\xc3\x87\xcf\x0f\x47\x63\x80\x91\xb0\x62\x72\x4c\xa5\x9e\x5a\ +\xce\x36\xf2\x2c\xeb\xa8\xec\x11\x60\x8d\x6c\x37\x8c\xcb\x36\x1c\ +\xf5\x5d\xfc\x28\x14\xd3\x79\x19\x4e\xbc\x1f\x30\xa5\x19\x16\xd9\ +\x12\xa7\x77\x5e\x84\x23\x1c\xe0\x24\xc0\xf4\xc7\x2e\x6a\x2c\xa4\ +\x10\x9a\xbb\x93\x4b\x5f\xee\x37\x30\x50\x34\x76\x57\x89\xd2\x87\ +\xef\x59\x27\x4c\x8e\x26\x9d\x82\xd7\xac\x05\x84\x96\x30\xa8\xb5\ +\x34\xde\xf0\x19\x00\x45\xfa\xa2\x5a\x6b\xf4\x93\x0c\xea\x4a\xc9\ +\xaf\xba\x82\x40\xa3\xb7\x4a\x5c\x9a\xa0\xec\x2b\x96\x5d\x25\xec\ +\xba\xf4\xa3\x89\xae\xf4\xf9\xd6\x18\xa0\xcd\x2f\xba\xd6\xa6\x33\ +\x2c\xab\xba\x76\xc4\x53\x7d\xc2\xe7\x4a\xee\x7e\xd6\xfb\xd2\xc7\ +\x1e\xf6\xa2\x04\x5e\x9d\x7a\x2d\xab\xd5\xda\xc2\x61\xbd\x38\x1e\ +\x9d\xf8\x0b\x10\xe7\x8d\xb4\x4c\x30\xcb\x9c\xa9\xef\x78\xd4\x90\ +\xa8\xf3\x00\x1f\x20\x52\xf1\xc7\xc3\xc7\x5e\x02\x99\xf3\xe2\x4f\ +\x6d\x9e\x06\x11\x1e\xe7\xc0\x1e\xf5\x16\xb2\xf8\x8b\xa3\x7a\x2e\ +\x78\x01\xa3\x6f\x98\x5d\x54\x0d\x5b\x49\xc7\x9a\x9f\x60\xd4\xf3\ +\xf0\x64\x38\x73\xb6\x87\x8d\x3c\x1d\xda\x1f\x6b\x64\xcf\xa1\xab\ +\x17\x49\xf0\xa6\x72\x41\xe1\x27\x0b\x50\xfc\xe6\x92\xf8\x5e\x78\ +\x71\x68\x65\x6b\xb9\xe8\x72\xf6\x5f\xc7\x2e\xc2\xa1\x8d\xa1\xc9\ +\x70\xd2\xe5\x3c\x1c\x41\xe8\x24\x21\x97\xed\x09\x1f\x27\x11\x3c\ +\xe4\x99\x1f\x49\xe4\x18\x24\xdc\x08\x06\xcf\x91\x92\xc3\xd4\x82\ +\x9a\x1d\xa4\x99\xf0\x69\xe2\xb2\xab\x94\xd0\xbb\xa0\x61\xfb\xaa\ +\xa9\xbe\x49\xc8\x81\x5f\x71\x39\x00\xfd\xf2\xf4\x15\x23\x4c\x9e\ +\xc4\xf9\xec\xd2\xca\x5d\x50\x84\x32\x1c\xc7\xf1\x8b\xd4\x7c\xdf\ +\x19\x93\x59\x7c\x25\xf2\x9b\xd6\x0d\x30\x85\xa2\x8c\xe6\xc9\x31\ +\x6d\xbb\xd6\xbb\x95\x71\x48\x2c\x59\xdd\x4a\xff\x84\xcc\x11\x82\ +\xfc\x95\x0a\x47\x2a\x12\x12\xbd\xe6\x32\xd0\x93\xd0\x59\x72\xe9\ +\xb1\x81\xf1\xb6\x6b\x06\x5e\xe3\xcd\xb8\x30\x6a\x58\x0d\xc0\x87\ +\x13\x7c\xab\x6d\x25\xa4\xd5\xb5\x34\x4c\x39\x0a\x91\xa3\x01\xea\ +\xbf\xa6\xd7\x8f\x5f\x25\xb7\xd7\x2c\x8f\xda\x08\x69\x8e\x19\xc8\ +\x99\x9a\xbb\x2b\x59\xfa\xb7\x59\xf7\x0c\x26\x57\xd1\x68\x58\x76\ +\xcf\xc8\x99\x0d\x6c\x8a\x01\x4b\x7b\xa8\x21\x8b\xbb\xc8\x85\xbb\ +\xc7\xf7\xfd\x5d\x2f\x6e\x71\xf7\xdc\x4f\xd7\x41\x11\xdd\x9f\xef\ +\x8d\xa2\x5b\x1b\xf7\x17\x7b\xa6\x80\x2b\x21\xcd\xa8\xed\x84\x1a\ +\x68\x65\x8b\x64\xd4\x09\x06\x28\x1a\xc3\x0e\xd3\x94\xc1\xd0\x5a\ +\x31\x76\x06\xf3\xfc\xdd\x4b\x50\x84\x73\x9a\xe5\xf4\x2e\x42\x69\ +\x4a\x71\x1a\x13\xef\x57\x2f\x22\x28\x4d\x28\x8d\xc8\x64\xf6\xae\ +\xa7\x3c\x7d\x49\x3e\xad\xa0\x56\xab\x61\x2f\x6e\xcd\x33\xb3\x5b\ +\x23\xdd\x78\xfe\xcf\x40\x10\x44\x31\xc5\x79\xf6\x26\x10\xf4\xfd\ +\x03\x41\x30\x8a\x28\xc5\xd9\xec\x2d\x20\x72\xf2\x5e\x81\x98\xa1\ +\x2c\x89\x93\x7c\x96\xdd\x65\x28\x89\x00\x87\x37\x61\x78\xb1\xb3\ +\xdf\x1f\x0c\x24\x41\x24\xa3\x78\x16\xbf\x09\xc4\xbf\xde\x10\x7f\ +\x15\x70\x79\x03\xd3\x53\x95\x8d\x07\x3f\x71\x18\xb8\x98\x02\x6b\ +\x73\x84\x63\xa8\x92\x7a\x6b\x8f\xa2\x24\x23\xb3\x6c\xfc\x2d\xf9\ +\x47\x50\xc3\x16\x48\xd3\x28\xff\x8f\x00\x77\x68\xcc\xdd\xff\xa7\ +\xc5\xcd\x77\xd2\xf3\xe7\xb2\ +\x00\x00\x03\x9b\ +\x3c\ +\x73\x76\x67\x20\x77\x69\x64\x74\x68\x3d\x22\x38\x30\x22\x20\x68\ +\x65\x69\x67\x68\x74\x3d\x22\x38\x30\x22\x20\x76\x69\x65\x77\x42\ +\x6f\x78\x3d\x22\x30\x20\x30\x20\x38\x30\x20\x38\x30\x22\x20\x66\ +\x69\x6c\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\x20\x78\x6d\x6c\x6e\x73\ +\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\ +\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x3e\x0a\x20\ +\x20\x3c\x70\x61\x74\x68\x20\x66\x69\x6c\x6c\x2d\x72\x75\x6c\x65\ +\x3d\x22\x65\x76\x65\x6e\x6f\x64\x64\x22\x20\x63\x6c\x69\x70\x2d\ +\x72\x75\x6c\x65\x3d\x22\x65\x76\x65\x6e\x6f\x64\x64\x22\x20\x64\ +\x3d\x22\x4d\x33\x33\x2e\x39\x32\x35\x36\x20\x39\x2e\x38\x38\x36\ +\x34\x37\x43\x33\x33\x2e\x31\x36\x34\x38\x20\x39\x2e\x38\x38\x36\ +\x34\x37\x20\x33\x32\x2e\x34\x34\x35\x34\x20\x31\x30\x2e\x32\x33\ +\x32\x39\x20\x33\x31\x2e\x39\x37\x31\x31\x20\x31\x30\x2e\x38\x32\ +\x37\x37\x4c\x32\x36\x2e\x36\x35\x30\x31\x20\x31\x37\x2e\x35\x48\ +\x31\x35\x43\x31\x33\x2e\x36\x31\x39\x33\x20\x31\x37\x2e\x35\x20\ +\x31\x32\x2e\x35\x20\x31\x38\x2e\x36\x31\x39\x33\x20\x31\x32\x2e\ +\x35\x20\x32\x30\x43\x31\x32\x2e\x35\x20\x32\x31\x2e\x33\x38\x30\ +\x37\x20\x31\x33\x2e\x36\x31\x39\x33\x20\x32\x32\x2e\x35\x20\x31\ +\x35\x20\x32\x32\x2e\x35\x48\x31\x36\x2e\x35\x56\x36\x34\x43\x31\ +\x36\x2e\x35\x20\x36\x37\x2e\x35\x38\x39\x39\x20\x31\x39\x2e\x34\ +\x31\x30\x31\x20\x37\x30\x2e\x35\x20\x32\x33\x20\x37\x30\x2e\x35\ +\x48\x35\x37\x43\x36\x30\x2e\x35\x38\x39\x38\x20\x37\x30\x2e\x35\ +\x20\x36\x33\x2e\x35\x20\x36\x37\x2e\x35\x38\x39\x39\x20\x36\x33\ +\x2e\x35\x20\x36\x34\x56\x32\x32\x2e\x35\x48\x36\x35\x43\x36\x36\ +\x2e\x33\x38\x30\x37\x20\x32\x32\x2e\x35\x20\x36\x37\x2e\x35\x20\ +\x32\x31\x2e\x33\x38\x30\x37\x20\x36\x37\x2e\x35\x20\x32\x30\x43\ +\x36\x37\x2e\x35\x20\x31\x38\x2e\x36\x31\x39\x33\x20\x36\x36\x2e\ +\x33\x38\x30\x37\x20\x31\x37\x2e\x35\x20\x36\x35\x20\x31\x37\x2e\ +\x35\x48\x35\x33\x2e\x33\x34\x39\x39\x4c\x34\x38\x2e\x30\x32\x39\ +\x20\x31\x30\x2e\x38\x32\x37\x38\x43\x34\x37\x2e\x35\x35\x34\x36\ +\x20\x31\x30\x2e\x32\x33\x32\x39\x20\x34\x36\x2e\x38\x33\x35\x32\ +\x20\x39\x2e\x38\x38\x36\x34\x37\x20\x34\x36\x2e\x30\x37\x34\x34\ +\x20\x39\x2e\x38\x38\x36\x34\x37\x48\x33\x33\x2e\x39\x32\x35\x36\ +\x5a\x4d\x33\x33\x20\x32\x37\x2e\x35\x43\x33\x34\x2e\x33\x38\x30\ +\x37\x20\x32\x37\x2e\x35\x20\x33\x35\x2e\x35\x20\x32\x38\x2e\x36\ +\x31\x39\x33\x20\x33\x35\x2e\x35\x20\x33\x30\x56\x35\x38\x43\x33\ +\x35\x2e\x35\x20\x35\x39\x2e\x33\x38\x30\x37\x20\x33\x34\x2e\x33\ +\x38\x30\x37\x20\x36\x30\x2e\x35\x20\x33\x33\x20\x36\x30\x2e\x35\ +\x43\x33\x31\x2e\x36\x31\x39\x33\x20\x36\x30\x2e\x35\x20\x33\x30\ +\x2e\x35\x20\x35\x39\x2e\x33\x38\x30\x37\x20\x33\x30\x2e\x35\x20\ +\x35\x38\x56\x33\x30\x43\x33\x30\x2e\x35\x20\x32\x38\x2e\x36\x31\ +\x39\x33\x20\x33\x31\x2e\x36\x31\x39\x33\x20\x32\x37\x2e\x35\x20\ +\x33\x33\x20\x32\x37\x2e\x35\x5a\x4d\x34\x39\x2e\x35\x20\x33\x30\ +\x43\x34\x39\x2e\x35\x20\x32\x38\x2e\x36\x31\x39\x33\x20\x34\x38\ +\x2e\x33\x38\x30\x37\x20\x32\x37\x2e\x35\x20\x34\x37\x20\x32\x37\ +\x2e\x35\x43\x34\x35\x2e\x36\x31\x39\x33\x20\x32\x37\x2e\x35\x20\ +\x34\x34\x2e\x35\x20\x32\x38\x2e\x36\x31\x39\x33\x20\x34\x34\x2e\ +\x35\x20\x33\x30\x56\x35\x38\x43\x34\x34\x2e\x35\x20\x35\x39\x2e\ +\x33\x38\x30\x37\x20\x34\x35\x2e\x36\x31\x39\x33\x20\x36\x30\x2e\ +\x35\x20\x34\x37\x20\x36\x30\x2e\x35\x43\x34\x38\x2e\x33\x38\x30\ +\x37\x20\x36\x30\x2e\x35\x20\x34\x39\x2e\x35\x20\x35\x39\x2e\x33\ +\x38\x30\x37\x20\x34\x39\x2e\x35\x20\x35\x38\x56\x33\x30\x5a\x4d\ +\x34\x36\x2e\x39\x35\x33\x36\x20\x31\x37\x2e\x34\x39\x38\x36\x4c\ +\x34\x34\x2e\x38\x37\x30\x34\x20\x31\x34\x2e\x38\x38\x36\x35\x48\ +\x33\x35\x2e\x31\x32\x39\x36\x4c\x33\x33\x2e\x30\x34\x36\x34\x20\ +\x31\x37\x2e\x34\x39\x38\x36\x48\x34\x36\x2e\x39\x35\x33\x36\x5a\ +\x22\x20\x66\x69\x6c\x6c\x3d\x22\x23\x43\x32\x43\x43\x44\x45\x22\ +\x20\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\x3e\ +\x00\x00\x01\x88\ +\x3c\ +\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ +\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ +\x30\x2f\x73\x76\x67\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x32\x34\ +\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x32\x34\x22\x20\x76\x69\ +\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\x30\x20\x32\x34\x20\x32\x34\ +\x22\x20\x66\x69\x6c\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\x20\x73\x74\ +\x72\x6f\x6b\x65\x3d\x22\x63\x75\x72\x72\x65\x6e\x74\x43\x6f\x6c\ +\x6f\x72\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\x68\ +\x3d\x22\x32\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\ +\x63\x61\x70\x3d\x22\x72\x6f\x75\x6e\x64\x22\x20\x73\x74\x72\x6f\ +\x6b\x65\x2d\x6c\x69\x6e\x65\x6a\x6f\x69\x6e\x3d\x22\x72\x6f\x75\ +\x6e\x64\x22\x20\x63\x6c\x61\x73\x73\x3d\x22\x66\x65\x61\x74\x68\ +\x65\x72\x20\x66\x65\x61\x74\x68\x65\x72\x2d\x73\x61\x76\x65\x22\ +\x3e\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\x31\x39\x20\x32\x31\ +\x48\x35\x61\x32\x20\x32\x20\x30\x20\x30\x20\x31\x2d\x32\x2d\x32\ +\x56\x35\x61\x32\x20\x32\x20\x30\x20\x30\x20\x31\x20\x32\x2d\x32\ +\x68\x31\x31\x6c\x35\x20\x35\x76\x31\x31\x61\x32\x20\x32\x20\x30\ +\x20\x30\x20\x31\x2d\x32\x20\x32\x7a\x22\x3e\x3c\x2f\x70\x61\x74\ +\x68\x3e\x3c\x70\x6f\x6c\x79\x6c\x69\x6e\x65\x20\x70\x6f\x69\x6e\ +\x74\x73\x3d\x22\x31\x37\x20\x32\x31\x20\x31\x37\x20\x31\x33\x20\ +\x37\x20\x31\x33\x20\x37\x20\x32\x31\x22\x3e\x3c\x2f\x70\x6f\x6c\ +\x79\x6c\x69\x6e\x65\x3e\x3c\x70\x6f\x6c\x79\x6c\x69\x6e\x65\x20\ +\x70\x6f\x69\x6e\x74\x73\x3d\x22\x37\x20\x33\x20\x37\x20\x38\x20\ +\x31\x35\x20\x38\x22\x3e\x3c\x2f\x70\x6f\x6c\x79\x6c\x69\x6e\x65\ +\x3e\x3c\x2f\x73\x76\x67\x3e\ +\x00\x00\x01\x70\ +\x3c\ +\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ +\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ +\x30\x2f\x73\x76\x67\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x32\x34\ +\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x32\x34\x22\x20\x76\x69\ +\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\x30\x20\x32\x34\x20\x32\x34\ +\x22\x20\x66\x69\x6c\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\x20\x73\x74\ +\x72\x6f\x6b\x65\x3d\x22\x63\x75\x72\x72\x65\x6e\x74\x43\x6f\x6c\ +\x6f\x72\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\x68\ +\x3d\x22\x32\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\ +\x63\x61\x70\x3d\x22\x72\x6f\x75\x6e\x64\x22\x20\x73\x74\x72\x6f\ +\x6b\x65\x2d\x6c\x69\x6e\x65\x6a\x6f\x69\x6e\x3d\x22\x72\x6f\x75\ +\x6e\x64\x22\x20\x63\x6c\x61\x73\x73\x3d\x22\x66\x65\x61\x74\x68\ +\x65\x72\x20\x66\x65\x61\x74\x68\x65\x72\x2d\x6c\x6f\x67\x2d\x69\ +\x6e\x22\x3e\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\x31\x35\x20\ +\x33\x68\x34\x61\x32\x20\x32\x20\x30\x20\x30\x20\x31\x20\x32\x20\ +\x32\x76\x31\x34\x61\x32\x20\x32\x20\x30\x20\x30\x20\x31\x2d\x32\ +\x20\x32\x68\x2d\x34\x22\x3e\x3c\x2f\x70\x61\x74\x68\x3e\x3c\x70\ +\x6f\x6c\x79\x6c\x69\x6e\x65\x20\x70\x6f\x69\x6e\x74\x73\x3d\x22\ +\x31\x30\x20\x31\x37\x20\x31\x35\x20\x31\x32\x20\x31\x30\x20\x37\ +\x22\x3e\x3c\x2f\x70\x6f\x6c\x79\x6c\x69\x6e\x65\x3e\x3c\x6c\x69\ +\x6e\x65\x20\x78\x31\x3d\x22\x31\x35\x22\x20\x79\x31\x3d\x22\x31\ +\x32\x22\x20\x78\x32\x3d\x22\x33\x22\x20\x79\x32\x3d\x22\x31\x32\ +\x22\x3e\x3c\x2f\x6c\x69\x6e\x65\x3e\x3c\x2f\x73\x76\x67\x3e\ +\x00\x00\x05\xba\ +\x3c\ +\x73\x76\x67\x20\x77\x69\x64\x74\x68\x3d\x22\x38\x30\x22\x20\x68\ +\x65\x69\x67\x68\x74\x3d\x22\x38\x30\x22\x20\x76\x69\x65\x77\x42\ +\x6f\x78\x3d\x22\x30\x20\x30\x20\x38\x30\x20\x38\x30\x22\x20\x66\ +\x69\x6c\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\x20\x78\x6d\x6c\x6e\x73\ +\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\ +\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x3e\x0a\x20\ +\x20\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\x33\x38\x2e\x31\x34\ +\x34\x31\x20\x31\x32\x2e\x36\x32\x31\x37\x43\x33\x38\x2e\x38\x31\ +\x35\x39\x20\x31\x30\x2e\x39\x34\x38\x38\x20\x34\x31\x2e\x31\x38\ +\x34\x31\x20\x31\x30\x2e\x39\x34\x38\x38\x20\x34\x31\x2e\x38\x35\ +\x35\x39\x20\x31\x32\x2e\x36\x32\x31\x37\x4c\x34\x37\x2e\x38\x30\ +\x38\x31\x20\x32\x37\x2e\x34\x34\x33\x39\x43\x34\x38\x2e\x30\x39\ +\x34\x32\x20\x32\x38\x2e\x31\x35\x36\x33\x20\x34\x38\x2e\x37\x36\ +\x32\x38\x20\x32\x38\x2e\x36\x34\x32\x20\x34\x39\x2e\x35\x32\x38\ +\x38\x20\x32\x38\x2e\x36\x39\x34\x4c\x36\x35\x2e\x34\x36\x34\x38\ +\x20\x32\x39\x2e\x37\x37\x34\x35\x43\x36\x37\x2e\x32\x36\x33\x35\ +\x20\x32\x39\x2e\x38\x39\x36\x35\x20\x36\x37\x2e\x39\x39\x35\x33\ +\x20\x33\x32\x2e\x31\x34\x38\x39\x20\x36\x36\x2e\x36\x31\x31\x38\ +\x20\x33\x33\x2e\x33\x30\x34\x37\x4c\x35\x34\x2e\x33\x35\x34\x34\ +\x20\x34\x33\x2e\x35\x34\x35\x39\x43\x35\x33\x2e\x37\x36\x35\x33\ +\x20\x34\x34\x2e\x30\x33\x38\x31\x20\x35\x33\x2e\x35\x30\x39\x39\ +\x20\x34\x34\x2e\x38\x32\x34\x31\x20\x35\x33\x2e\x36\x39\x37\x32\ +\x20\x34\x35\x2e\x35\x36\x38\x36\x4c\x35\x37\x2e\x35\x39\x34\x31\ +\x20\x36\x31\x2e\x30\x35\x38\x36\x43\x35\x38\x2e\x30\x33\x33\x39\ +\x20\x36\x32\x2e\x38\x30\x36\x39\x20\x35\x36\x2e\x31\x31\x37\x39\ +\x20\x36\x34\x2e\x31\x39\x39\x20\x35\x34\x2e\x35\x39\x31\x31\x20\ +\x36\x33\x2e\x32\x34\x30\x34\x4c\x34\x31\x2e\x30\x36\x33\x34\x20\ +\x35\x34\x2e\x37\x34\x37\x36\x43\x34\x30\x2e\x34\x31\x33\x32\x20\ +\x35\x34\x2e\x33\x33\x39\x34\x20\x33\x39\x2e\x35\x38\x36\x38\x20\ +\x35\x34\x2e\x33\x33\x39\x34\x20\x33\x38\x2e\x39\x33\x36\x36\x20\ +\x35\x34\x2e\x37\x34\x37\x36\x4c\x32\x35\x2e\x34\x30\x38\x39\x20\ +\x36\x33\x2e\x32\x34\x30\x34\x43\x32\x33\x2e\x38\x38\x32\x31\x20\ +\x36\x34\x2e\x31\x39\x39\x20\x32\x31\x2e\x39\x36\x36\x31\x20\x36\ +\x32\x2e\x38\x30\x36\x39\x20\x32\x32\x2e\x34\x30\x35\x39\x20\x36\ +\x31\x2e\x30\x35\x38\x36\x4c\x32\x36\x2e\x33\x30\x32\x38\x20\x34\ +\x35\x2e\x35\x36\x38\x36\x43\x32\x36\x2e\x34\x39\x30\x31\x20\x34\ +\x34\x2e\x38\x32\x34\x31\x20\x32\x36\x2e\x32\x33\x34\x37\x20\x34\ +\x34\x2e\x30\x33\x38\x31\x20\x32\x35\x2e\x36\x34\x35\x36\x20\x34\ +\x33\x2e\x35\x34\x35\x39\x4c\x31\x33\x2e\x33\x38\x38\x32\x20\x33\ +\x33\x2e\x33\x30\x34\x37\x43\x31\x32\x2e\x30\x30\x34\x37\x20\x33\ +\x32\x2e\x31\x34\x38\x39\x20\x31\x32\x2e\x37\x33\x36\x35\x20\x32\ +\x39\x2e\x38\x39\x36\x35\x20\x31\x34\x2e\x35\x33\x35\x32\x20\x32\ +\x39\x2e\x37\x37\x34\x35\x4c\x33\x30\x2e\x34\x37\x31\x32\x20\x32\ +\x38\x2e\x36\x39\x34\x43\x33\x31\x2e\x32\x33\x37\x32\x20\x32\x38\ +\x2e\x36\x34\x32\x20\x33\x31\x2e\x39\x30\x35\x38\x20\x32\x38\x2e\ +\x31\x35\x36\x33\x20\x33\x32\x2e\x31\x39\x31\x39\x20\x32\x37\x2e\ +\x34\x34\x33\x39\x4c\x33\x38\x2e\x31\x34\x34\x31\x20\x31\x32\x2e\ +\x36\x32\x31\x37\x5a\x22\x20\x66\x69\x6c\x6c\x3d\x22\x23\x46\x32\ +\x39\x39\x34\x41\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x70\x61\x74\x68\ +\x20\x64\x3d\x22\x4d\x33\x39\x2e\x30\x35\x31\x35\x20\x32\x36\x2e\ +\x33\x31\x30\x38\x43\x33\x39\x2e\x33\x38\x37\x34\x20\x32\x35\x2e\ +\x34\x37\x34\x34\x20\x34\x30\x2e\x35\x37\x31\x36\x20\x32\x35\x2e\ +\x34\x37\x34\x34\x20\x34\x30\x2e\x39\x30\x37\x35\x20\x32\x36\x2e\ +\x33\x31\x30\x38\x4c\x34\x33\x2e\x38\x38\x33\x35\x20\x33\x33\x2e\ +\x37\x32\x31\x39\x43\x34\x34\x2e\x30\x32\x36\x36\x20\x33\x34\x2e\ +\x30\x37\x38\x31\x20\x34\x34\x2e\x33\x36\x30\x39\x20\x33\x34\x2e\ +\x33\x32\x31\x20\x34\x34\x2e\x37\x34\x33\x39\x20\x33\x34\x2e\x33\ +\x34\x37\x4c\x35\x32\x2e\x37\x31\x31\x39\x20\x33\x34\x2e\x38\x38\ +\x37\x33\x43\x35\x33\x2e\x36\x31\x31\x32\x20\x33\x34\x2e\x39\x34\ +\x38\x32\x20\x35\x33\x2e\x39\x37\x37\x31\x20\x33\x36\x2e\x30\x37\ +\x34\x34\x20\x35\x33\x2e\x32\x38\x35\x34\x20\x33\x36\x2e\x36\x35\ +\x32\x34\x4c\x34\x37\x2e\x31\x35\x36\x37\x20\x34\x31\x2e\x37\x37\ +\x32\x39\x43\x34\x36\x2e\x38\x36\x32\x31\x20\x34\x32\x2e\x30\x31\ +\x39\x31\x20\x34\x36\x2e\x37\x33\x34\x34\x20\x34\x32\x2e\x34\x31\ +\x32\x31\x20\x34\x36\x2e\x38\x32\x38\x31\x20\x34\x32\x2e\x37\x38\ +\x34\x33\x4c\x34\x38\x2e\x37\x37\x36\x35\x20\x35\x30\x2e\x35\x32\ +\x39\x33\x43\x34\x38\x2e\x39\x39\x36\x34\x20\x35\x31\x2e\x34\x30\ +\x33\x35\x20\x34\x38\x2e\x30\x33\x38\x34\x20\x35\x32\x2e\x30\x39\ +\x39\x35\x20\x34\x37\x2e\x32\x37\x35\x20\x35\x31\x2e\x36\x32\x30\ +\x32\x4c\x34\x30\x2e\x35\x31\x31\x32\x20\x34\x37\x2e\x33\x37\x33\ +\x38\x43\x34\x30\x2e\x31\x38\x36\x31\x20\x34\x37\x2e\x31\x36\x39\ +\x37\x20\x33\x39\x2e\x37\x37\x32\x39\x20\x34\x37\x2e\x31\x36\x39\ +\x37\x20\x33\x39\x2e\x34\x34\x37\x38\x20\x34\x37\x2e\x33\x37\x33\ +\x38\x4c\x33\x32\x2e\x36\x38\x34\x20\x35\x31\x2e\x36\x32\x30\x32\ +\x43\x33\x31\x2e\x39\x32\x30\x35\x20\x35\x32\x2e\x30\x39\x39\x35\ +\x20\x33\x30\x2e\x39\x36\x32\x36\x20\x35\x31\x2e\x34\x30\x33\x35\ +\x20\x33\x31\x2e\x31\x38\x32\x35\x20\x35\x30\x2e\x35\x32\x39\x33\ +\x4c\x33\x33\x2e\x31\x33\x30\x39\x20\x34\x32\x2e\x37\x38\x34\x33\ +\x43\x33\x33\x2e\x32\x32\x34\x35\x20\x34\x32\x2e\x34\x31\x32\x31\ +\x20\x33\x33\x2e\x30\x39\x36\x38\x20\x34\x32\x2e\x30\x31\x39\x31\ +\x20\x33\x32\x2e\x38\x30\x32\x33\x20\x34\x31\x2e\x37\x37\x32\x39\ +\x4c\x32\x36\x2e\x36\x37\x33\x36\x20\x33\x36\x2e\x36\x35\x32\x34\ +\x43\x32\x35\x2e\x39\x38\x31\x38\x20\x33\x36\x2e\x30\x37\x34\x34\ +\x20\x32\x36\x2e\x33\x34\x37\x38\x20\x33\x34\x2e\x39\x34\x38\x32\ +\x20\x32\x37\x2e\x32\x34\x37\x31\x20\x33\x34\x2e\x38\x38\x37\x33\ +\x4c\x33\x35\x2e\x32\x31\x35\x31\x20\x33\x34\x2e\x33\x34\x37\x43\ +\x33\x35\x2e\x35\x39\x38\x31\x20\x33\x34\x2e\x33\x32\x31\x20\x33\ +\x35\x2e\x39\x33\x32\x34\x20\x33\x34\x2e\x30\x37\x38\x31\x20\x33\ +\x36\x2e\x30\x37\x35\x34\x20\x33\x33\x2e\x37\x32\x31\x39\x4c\x33\ +\x39\x2e\x30\x35\x31\x35\x20\x32\x36\x2e\x33\x31\x30\x38\x5a\x22\ +\x20\x66\x69\x6c\x6c\x3d\x22\x23\x46\x32\x43\x39\x34\x43\x22\x20\ +\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\x3e\ +\x00\x00\x04\xc2\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x75\x74\x66\ +\x2d\x38\x22\x3f\x3e\x3c\x21\x2d\x2d\x20\x55\x70\x6c\x6f\x61\x64\ +\x65\x64\x20\x74\x6f\x3a\x20\x53\x56\x47\x20\x52\x65\x70\x6f\x2c\ +\x20\x77\x77\x77\x2e\x73\x76\x67\x72\x65\x70\x6f\x2e\x63\x6f\x6d\ +\x2c\x20\x47\x65\x6e\x65\x72\x61\x74\x6f\x72\x3a\x20\x53\x56\x47\ +\x20\x52\x65\x70\x6f\x20\x4d\x69\x78\x65\x72\x20\x54\x6f\x6f\x6c\ +\x73\x20\x2d\x2d\x3e\x0a\x3c\x73\x76\x67\x20\x77\x69\x64\x74\x68\ +\x3d\x22\x38\x30\x30\x70\x78\x22\x20\x68\x65\x69\x67\x68\x74\x3d\ +\x22\x38\x30\x30\x70\x78\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\ +\x22\x30\x20\x30\x20\x32\x34\x20\x32\x34\x22\x20\x66\x69\x6c\x6c\ +\x3d\x22\x6e\x6f\x6e\x65\x22\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\ +\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\ +\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x3e\x0a\x3c\x70\x61\x74\ +\x68\x20\x64\x3d\x22\x4d\x32\x30\x20\x39\x2e\x35\x30\x31\x39\x35\ +\x56\x38\x2e\x37\x34\x39\x38\x35\x43\x32\x30\x20\x37\x2e\x35\x30\ +\x37\x32\x31\x20\x31\x38\x2e\x39\x39\x32\x36\x20\x36\x2e\x34\x39\ +\x39\x38\x35\x20\x31\x37\x2e\x37\x35\x20\x36\x2e\x34\x39\x39\x38\ +\x35\x48\x31\x32\x2e\x30\x32\x34\x37\x4c\x39\x2e\x36\x34\x33\x36\ +\x38\x20\x34\x2e\x35\x31\x39\x39\x35\x43\x39\x2e\x32\x33\x39\x35\ +\x39\x20\x34\x2e\x31\x38\x33\x39\x33\x20\x38\x2e\x37\x33\x30\x36\ +\x33\x20\x33\x2e\x39\x39\x39\x39\x37\x20\x38\x2e\x32\x30\x35\x30\ +\x39\x20\x33\x2e\x39\x39\x39\x39\x37\x48\x34\x2e\x32\x34\x39\x35\ +\x37\x43\x33\x2e\x30\x30\x37\x32\x34\x20\x33\x2e\x39\x39\x39\x39\ +\x37\x20\x32\x20\x35\x2e\x30\x30\x36\x38\x36\x20\x31\x2e\x39\x39\ +\x39\x35\x37\x20\x36\x2e\x32\x34\x39\x31\x39\x4c\x31\x2e\x39\x39\ +\x35\x36\x31\x20\x31\x37\x2e\x37\x34\x39\x32\x43\x31\x2e\x39\x39\ +\x35\x31\x38\x20\x31\x38\x2e\x39\x39\x32\x31\x20\x33\x2e\x30\x30\ +\x32\x36\x36\x20\x32\x30\x20\x34\x2e\x32\x34\x35\x36\x31\x20\x32\ +\x30\x48\x34\x2e\x32\x37\x31\x39\x36\x43\x34\x2e\x32\x37\x36\x30\ +\x37\x20\x32\x30\x20\x34\x2e\x32\x38\x30\x31\x39\x20\x32\x30\x20\ +\x34\x2e\x32\x38\x34\x33\x31\x20\x32\x30\x48\x31\x38\x2e\x34\x36\ +\x39\x33\x43\x31\x39\x2e\x32\x37\x32\x33\x20\x32\x30\x20\x31\x39\ +\x2e\x39\x37\x32\x33\x20\x31\x39\x2e\x34\x35\x33\x35\x20\x32\x30\ +\x2e\x31\x36\x37\x20\x31\x38\x2e\x36\x37\x34\x35\x4c\x32\x31\x2e\ +\x39\x31\x36\x39\x20\x31\x31\x2e\x36\x37\x36\x35\x43\x32\x32\x2e\ +\x31\x39\x33\x31\x20\x31\x30\x2e\x35\x37\x31\x39\x20\x32\x31\x2e\ +\x33\x35\x37\x37\x20\x39\x2e\x35\x30\x31\x39\x35\x20\x32\x30\x2e\ +\x32\x31\x39\x32\x20\x39\x2e\x35\x30\x31\x39\x35\x48\x32\x30\x5a\ +\x4d\x34\x2e\x32\x34\x39\x35\x37\x20\x35\x2e\x34\x39\x39\x39\x37\ +\x48\x38\x2e\x32\x30\x35\x30\x39\x43\x38\x2e\x33\x38\x30\x32\x37\ +\x20\x35\x2e\x34\x39\x39\x39\x37\x20\x38\x2e\x35\x34\x39\x39\x33\ +\x20\x35\x2e\x35\x36\x31\x32\x39\x20\x38\x2e\x36\x38\x34\x36\x32\ +\x20\x35\x2e\x36\x37\x33\x33\x4c\x31\x31\x2e\x32\x37\x34\x31\x20\ +\x37\x2e\x38\x32\x36\x35\x32\x43\x31\x31\x2e\x34\x30\x38\x38\x20\ +\x37\x2e\x39\x33\x38\x35\x32\x20\x31\x31\x2e\x35\x37\x38\x34\x20\ +\x37\x2e\x39\x39\x39\x38\x35\x20\x31\x31\x2e\x37\x35\x33\x36\x20\ +\x37\x2e\x39\x39\x39\x38\x35\x48\x31\x37\x2e\x37\x35\x43\x31\x38\ +\x2e\x31\x36\x34\x32\x20\x37\x2e\x39\x39\x39\x38\x35\x20\x31\x38\ +\x2e\x35\x20\x38\x2e\x33\x33\x35\x36\x33\x20\x31\x38\x2e\x35\x20\ +\x38\x2e\x37\x34\x39\x38\x35\x56\x39\x2e\x35\x30\x31\x39\x35\x48\ +\x36\x2e\x34\x32\x33\x38\x35\x43\x35\x2e\x33\x39\x31\x33\x36\x20\ +\x39\x2e\x35\x30\x31\x39\x35\x20\x34\x2e\x34\x39\x31\x33\x37\x20\ +\x31\x30\x2e\x32\x30\x34\x37\x20\x34\x2e\x32\x34\x31\x20\x31\x31\ +\x2e\x32\x30\x36\x34\x4c\x33\x2e\x34\x39\x36\x38\x34\x20\x31\x34\ +\x2e\x31\x38\x33\x37\x4c\x33\x2e\x34\x39\x39\x35\x37\x20\x36\x2e\ +\x32\x34\x39\x37\x31\x43\x33\x2e\x34\x39\x39\x37\x31\x20\x35\x2e\ +\x38\x33\x35\x36\x20\x33\x2e\x38\x33\x35\x34\x36\x20\x35\x2e\x34\ +\x39\x39\x39\x37\x20\x34\x2e\x32\x34\x39\x35\x37\x20\x35\x2e\x34\ +\x39\x39\x39\x37\x5a\x4d\x35\x2e\x36\x39\x36\x32\x33\x20\x31\x31\ +\x2e\x35\x37\x30\x31\x43\x35\x2e\x37\x37\x39\x36\x39\x20\x31\x31\ +\x2e\x32\x33\x36\x32\x20\x36\x2e\x30\x37\x39\x36\x39\x20\x31\x31\ +\x2e\x30\x30\x32\x20\x36\x2e\x34\x32\x33\x38\x35\x20\x31\x31\x2e\ +\x30\x30\x32\x48\x32\x30\x2e\x32\x31\x39\x32\x43\x32\x30\x2e\x33\ +\x38\x31\x39\x20\x31\x31\x2e\x30\x30\x32\x20\x32\x30\x2e\x35\x30\ +\x31\x32\x20\x31\x31\x2e\x31\x35\x34\x38\x20\x32\x30\x2e\x34\x36\ +\x31\x37\x20\x31\x31\x2e\x33\x31\x32\x36\x4c\x31\x38\x2e\x37\x31\ +\x31\x39\x20\x31\x38\x2e\x33\x31\x30\x37\x43\x31\x38\x2e\x36\x38\ +\x34\x20\x31\x38\x2e\x34\x32\x31\x39\x20\x31\x38\x2e\x35\x38\x34\ +\x20\x31\x38\x2e\x35\x20\x31\x38\x2e\x34\x36\x39\x33\x20\x31\x38\ +\x2e\x35\x48\x34\x2e\x32\x38\x34\x33\x31\x43\x34\x2e\x31\x32\x31\ +\x36\x37\x20\x31\x38\x2e\x35\x20\x34\x2e\x30\x30\x32\x33\x33\x20\ +\x31\x38\x2e\x33\x34\x37\x32\x20\x34\x2e\x30\x34\x31\x37\x37\x20\ +\x31\x38\x2e\x31\x38\x39\x34\x4c\x35\x2e\x36\x39\x36\x32\x33\x20\ +\x31\x31\x2e\x35\x37\x30\x31\x5a\x22\x20\x66\x69\x6c\x6c\x3d\x22\ +\x23\x32\x31\x32\x31\x32\x31\x22\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\ +\x3e\ +\x00\x00\x09\x8f\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\x20\x73\x74\x61\x6e\x64\x61\x6c\x6f\x6e\x65\x3d\x22\ +\x6e\x6f\x22\x3f\x3e\x0a\x3c\x73\x76\x67\x0a\x20\x20\x20\x78\x6d\ +\x6c\x6e\x73\x3a\x64\x63\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x70\ +\x75\x72\x6c\x2e\x6f\x72\x67\x2f\x64\x63\x2f\x65\x6c\x65\x6d\x65\ +\x6e\x74\x73\x2f\x31\x2e\x31\x2f\x22\x0a\x20\x20\x20\x78\x6d\x6c\ +\x6e\x73\x3a\x63\x63\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x63\x72\ +\x65\x61\x74\x69\x76\x65\x63\x6f\x6d\x6d\x6f\x6e\x73\x2e\x6f\x72\ +\x67\x2f\x6e\x73\x23\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\ +\x72\x64\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\ +\x77\x33\x2e\x6f\x72\x67\x2f\x31\x39\x39\x39\x2f\x30\x32\x2f\x32\ +\x32\x2d\x72\x64\x66\x2d\x73\x79\x6e\x74\x61\x78\x2d\x6e\x73\x23\ +\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x73\x76\x67\x3d\x22\ +\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\ +\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x0a\x20\x20\x20\x78\ +\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\ +\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\ +\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x73\x6f\x64\x69\x70\ +\x6f\x64\x69\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x73\x6f\x64\x69\ +\x70\x6f\x64\x69\x2e\x73\x6f\x75\x72\x63\x65\x66\x6f\x72\x67\x65\ +\x2e\x6e\x65\x74\x2f\x44\x54\x44\x2f\x73\x6f\x64\x69\x70\x6f\x64\ +\x69\x2d\x30\x2e\x64\x74\x64\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\ +\x73\x3a\x69\x6e\x6b\x73\x63\x61\x70\x65\x3d\x22\x68\x74\x74\x70\ +\x3a\x2f\x2f\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\x65\x2e\ +\x6f\x72\x67\x2f\x6e\x61\x6d\x65\x73\x70\x61\x63\x65\x73\x2f\x69\ +\x6e\x6b\x73\x63\x61\x70\x65\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\ +\x63\x61\x70\x65\x3a\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x72\x63\x31\x20\x28\x30\x39\x39\x36\x30\x64\x36\x66\x30\x35\ +\x2c\x20\x32\x30\x32\x30\x2d\x30\x34\x2d\x30\x39\x29\x22\x0a\x20\ +\x20\x20\x73\x6f\x64\x69\x70\x6f\x64\x69\x3a\x64\x6f\x63\x6e\x61\ +\x6d\x65\x3d\x22\x63\x6f\x6c\x6c\x61\x70\x73\x65\x2e\x73\x76\x67\ +\x22\x0a\x20\x20\x20\x69\x64\x3d\x22\x73\x76\x67\x36\x22\x0a\x20\ +\x20\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\x31\x22\x0a\ +\x20\x20\x20\x63\x6c\x61\x73\x73\x3d\x22\x66\x65\x61\x74\x68\x65\ +\x72\x20\x66\x65\x61\x74\x68\x65\x72\x2d\x63\x6f\x64\x65\x22\x0a\ +\x20\x20\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\x6a\x6f\ +\x69\x6e\x3d\x22\x72\x6f\x75\x6e\x64\x22\x0a\x20\x20\x20\x73\x74\ +\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\x63\x61\x70\x3d\x22\x72\x6f\ +\x75\x6e\x64\x22\x0a\x20\x20\x20\x73\x74\x72\x6f\x6b\x65\x2d\x77\ +\x69\x64\x74\x68\x3d\x22\x32\x22\x0a\x20\x20\x20\x73\x74\x72\x6f\ +\x6b\x65\x3d\x22\x63\x75\x72\x72\x65\x6e\x74\x43\x6f\x6c\x6f\x72\ +\x22\x0a\x20\x20\x20\x66\x69\x6c\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\ +\x0a\x20\x20\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\x30\ +\x20\x32\x34\x20\x32\x34\x22\x0a\x20\x20\x20\x68\x65\x69\x67\x68\ +\x74\x3d\x22\x32\x34\x22\x0a\x20\x20\x20\x77\x69\x64\x74\x68\x3d\ +\x22\x32\x34\x22\x3e\x0a\x20\x20\x3c\x6d\x65\x74\x61\x64\x61\x74\ +\x61\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x6d\x65\x74\x61\x64\ +\x61\x74\x61\x31\x32\x22\x3e\x0a\x20\x20\x20\x20\x3c\x72\x64\x66\ +\x3a\x52\x44\x46\x3e\x0a\x20\x20\x20\x20\x20\x20\x3c\x63\x63\x3a\ +\x57\x6f\x72\x6b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x72\x64\ +\x66\x3a\x61\x62\x6f\x75\x74\x3d\x22\x22\x3e\x0a\x20\x20\x20\x20\ +\x20\x20\x20\x20\x3c\x64\x63\x3a\x66\x6f\x72\x6d\x61\x74\x3e\x69\ +\x6d\x61\x67\x65\x2f\x73\x76\x67\x2b\x78\x6d\x6c\x3c\x2f\x64\x63\ +\x3a\x66\x6f\x72\x6d\x61\x74\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\ +\x20\x3c\x64\x63\x3a\x74\x79\x70\x65\x0a\x20\x20\x20\x20\x20\x20\ +\x20\x20\x20\x20\x20\x72\x64\x66\x3a\x72\x65\x73\x6f\x75\x72\x63\ +\x65\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x70\x75\x72\x6c\x2e\x6f\ +\x72\x67\x2f\x64\x63\x2f\x64\x63\x6d\x69\x74\x79\x70\x65\x2f\x53\ +\x74\x69\x6c\x6c\x49\x6d\x61\x67\x65\x22\x20\x2f\x3e\x0a\x20\x20\ +\x20\x20\x20\x20\x20\x20\x3c\x64\x63\x3a\x74\x69\x74\x6c\x65\x3e\ +\x3c\x2f\x64\x63\x3a\x74\x69\x74\x6c\x65\x3e\x0a\x20\x20\x20\x20\ +\x20\x20\x3c\x2f\x63\x63\x3a\x57\x6f\x72\x6b\x3e\x0a\x20\x20\x20\ +\x20\x3c\x2f\x72\x64\x66\x3a\x52\x44\x46\x3e\x0a\x20\x20\x3c\x2f\ +\x6d\x65\x74\x61\x64\x61\x74\x61\x3e\x0a\x20\x20\x3c\x64\x65\x66\ +\x73\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x64\x65\x66\x73\x31\ +\x30\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x73\x6f\x64\x69\x70\x6f\x64\ +\x69\x3a\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\x0a\x20\x20\x20\x20\ +\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x75\x72\x72\x65\x6e\ +\x74\x2d\x6c\x61\x79\x65\x72\x3d\x22\x73\x76\x67\x36\x22\x0a\x20\ +\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\ +\x64\x6f\x77\x2d\x6d\x61\x78\x69\x6d\x69\x7a\x65\x64\x3d\x22\x30\ +\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\ +\x77\x69\x6e\x64\x6f\x77\x2d\x79\x3d\x22\x31\x38\x35\x22\x0a\x20\ +\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\ +\x64\x6f\x77\x2d\x78\x3d\x22\x31\x33\x38\x37\x22\x0a\x20\x20\x20\ +\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x79\x3d\x22\x31\ +\x32\x2e\x31\x38\x37\x33\x38\x32\x22\x0a\x20\x20\x20\x20\x20\x69\ +\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x78\x3d\x22\x31\x32\x2e\x34\ +\x37\x33\x33\x38\x37\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\ +\x63\x61\x70\x65\x3a\x7a\x6f\x6f\x6d\x3d\x22\x32\x39\x2e\x36\x39\ +\x38\x34\x38\x35\x22\x0a\x20\x20\x20\x20\x20\x73\x68\x6f\x77\x67\ +\x72\x69\x64\x3d\x22\x66\x61\x6c\x73\x65\x22\x0a\x20\x20\x20\x20\ +\x20\x69\x64\x3d\x22\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\x38\x22\ +\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\ +\x69\x6e\x64\x6f\x77\x2d\x68\x65\x69\x67\x68\x74\x3d\x22\x31\x31\ +\x32\x38\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\ +\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x77\x69\x64\x74\x68\x3d\x22\ +\x31\x39\x37\x34\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\ +\x61\x70\x65\x3a\x70\x61\x67\x65\x73\x68\x61\x64\x6f\x77\x3d\x22\ +\x32\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\ +\x3a\x70\x61\x67\x65\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\x30\x22\ +\x0a\x20\x20\x20\x20\x20\x67\x75\x69\x64\x65\x74\x6f\x6c\x65\x72\ +\x61\x6e\x63\x65\x3d\x22\x31\x30\x22\x0a\x20\x20\x20\x20\x20\x67\ +\x72\x69\x64\x74\x6f\x6c\x65\x72\x61\x6e\x63\x65\x3d\x22\x31\x30\ +\x22\x0a\x20\x20\x20\x20\x20\x6f\x62\x6a\x65\x63\x74\x74\x6f\x6c\ +\x65\x72\x61\x6e\x63\x65\x3d\x22\x31\x30\x22\x0a\x20\x20\x20\x20\ +\x20\x62\x6f\x72\x64\x65\x72\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\ +\x31\x22\x0a\x20\x20\x20\x20\x20\x62\x6f\x72\x64\x65\x72\x63\x6f\ +\x6c\x6f\x72\x3d\x22\x23\x36\x36\x36\x36\x36\x36\x22\x0a\x20\x20\ +\x20\x20\x20\x70\x61\x67\x65\x63\x6f\x6c\x6f\x72\x3d\x22\x23\x66\ +\x66\x66\x66\x66\x66\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x70\x61\x74\ +\x68\x0a\x20\x20\x20\x20\x20\x73\x74\x79\x6c\x65\x3d\x22\x66\x69\ +\x6c\x6c\x3a\x6e\x6f\x6e\x65\x3b\x73\x74\x72\x6f\x6b\x65\x3a\x23\ +\x30\x30\x30\x30\x30\x30\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\ +\x64\x74\x68\x3a\x31\x2e\x38\x39\x34\x34\x33\x3b\x73\x74\x72\x6f\ +\x6b\x65\x2d\x6c\x69\x6e\x65\x63\x61\x70\x3a\x72\x6f\x75\x6e\x64\ +\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\x6a\x6f\x69\x6e\ +\x3a\x6d\x69\x74\x65\x72\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x6d\x69\ +\x74\x65\x72\x6c\x69\x6d\x69\x74\x3a\x34\x3b\x73\x74\x72\x6f\x6b\ +\x65\x2d\x64\x61\x73\x68\x61\x72\x72\x61\x79\x3a\x6e\x6f\x6e\x65\ +\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x6f\x70\x61\x63\x69\x74\x79\x3a\ +\x31\x22\x0a\x20\x20\x20\x20\x20\x64\x3d\x22\x4d\x20\x32\x2e\x38\ +\x39\x35\x37\x37\x30\x36\x2c\x33\x2e\x37\x33\x37\x35\x36\x34\x34\ +\x20\x48\x20\x32\x30\x2e\x32\x33\x36\x37\x32\x33\x22\x0a\x20\x20\ +\x20\x20\x20\x69\x64\x3d\x22\x70\x61\x74\x68\x38\x35\x37\x22\x0a\ +\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x6f\ +\x6e\x6e\x65\x63\x74\x6f\x72\x2d\x63\x75\x72\x76\x61\x74\x75\x72\ +\x65\x3d\x22\x30\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x70\x61\x74\x68\ +\x0a\x20\x20\x20\x20\x20\x73\x74\x79\x6c\x65\x3d\x22\x66\x69\x6c\ +\x6c\x3a\x6e\x6f\x6e\x65\x3b\x73\x74\x72\x6f\x6b\x65\x3a\x23\x30\ +\x30\x30\x30\x30\x30\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\ +\x74\x68\x3a\x31\x2e\x38\x39\x34\x34\x33\x3b\x73\x74\x72\x6f\x6b\ +\x65\x2d\x6c\x69\x6e\x65\x63\x61\x70\x3a\x72\x6f\x75\x6e\x64\x3b\ +\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\x6a\x6f\x69\x6e\x3a\ +\x6d\x69\x74\x65\x72\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x6d\x69\x74\ +\x65\x72\x6c\x69\x6d\x69\x74\x3a\x34\x3b\x73\x74\x72\x6f\x6b\x65\ +\x2d\x64\x61\x73\x68\x61\x72\x72\x61\x79\x3a\x6e\x6f\x6e\x65\x3b\ +\x73\x74\x72\x6f\x6b\x65\x2d\x6f\x70\x61\x63\x69\x74\x79\x3a\x31\ +\x22\x0a\x20\x20\x20\x20\x20\x64\x3d\x22\x4d\x20\x32\x2e\x38\x39\ +\x35\x37\x37\x30\x36\x2c\x31\x31\x2e\x34\x33\x37\x31\x37\x32\x20\ +\x48\x20\x32\x30\x2e\x32\x33\x36\x37\x32\x33\x22\x0a\x20\x20\x20\ +\x20\x20\x69\x64\x3d\x22\x70\x61\x74\x68\x38\x35\x39\x22\x0a\x20\ +\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x6f\x6e\ +\x6e\x65\x63\x74\x6f\x72\x2d\x63\x75\x72\x76\x61\x74\x75\x72\x65\ +\x3d\x22\x30\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x70\x61\x74\x68\x0a\ +\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x6f\ +\x6e\x6e\x65\x63\x74\x6f\x72\x2d\x63\x75\x72\x76\x61\x74\x75\x72\ +\x65\x3d\x22\x30\x22\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x70\ +\x61\x74\x68\x38\x35\x35\x22\x0a\x20\x20\x20\x20\x20\x64\x3d\x22\ +\x4d\x20\x32\x2e\x38\x39\x35\x37\x37\x30\x36\x2c\x37\x2e\x35\x38\ +\x37\x33\x36\x38\x31\x20\x48\x20\x32\x30\x2e\x32\x33\x36\x37\x32\ +\x33\x22\x0a\x20\x20\x20\x20\x20\x73\x74\x79\x6c\x65\x3d\x22\x66\ +\x69\x6c\x6c\x3a\x6e\x6f\x6e\x65\x3b\x73\x74\x72\x6f\x6b\x65\x3a\ +\x23\x30\x30\x30\x30\x30\x30\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x77\ +\x69\x64\x74\x68\x3a\x31\x2e\x38\x39\x34\x34\x33\x3b\x73\x74\x72\ +\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\x63\x61\x70\x3a\x72\x6f\x75\x6e\ +\x64\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\x6a\x6f\x69\ +\x6e\x3a\x6d\x69\x74\x65\x72\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x6d\ +\x69\x74\x65\x72\x6c\x69\x6d\x69\x74\x3a\x34\x3b\x73\x74\x72\x6f\ +\x6b\x65\x2d\x64\x61\x73\x68\x61\x72\x72\x61\x79\x3a\x6e\x6f\x6e\ +\x65\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x6f\x70\x61\x63\x69\x74\x79\ +\x3a\x31\x22\x20\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\x3e\x0a\ +\x00\x00\x07\x9d\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\x20\x73\x74\x61\x6e\x64\x61\x6c\x6f\x6e\x65\x3d\x22\ +\x6e\x6f\x22\x3f\x3e\x0a\x3c\x73\x76\x67\x0a\x20\x20\x20\x78\x6d\ +\x6c\x6e\x73\x3a\x64\x63\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x70\ +\x75\x72\x6c\x2e\x6f\x72\x67\x2f\x64\x63\x2f\x65\x6c\x65\x6d\x65\ +\x6e\x74\x73\x2f\x31\x2e\x31\x2f\x22\x0a\x20\x20\x20\x78\x6d\x6c\ +\x6e\x73\x3a\x63\x63\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x63\x72\ +\x65\x61\x74\x69\x76\x65\x63\x6f\x6d\x6d\x6f\x6e\x73\x2e\x6f\x72\ +\x67\x2f\x6e\x73\x23\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\ +\x72\x64\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\ +\x77\x33\x2e\x6f\x72\x67\x2f\x31\x39\x39\x39\x2f\x30\x32\x2f\x32\ +\x32\x2d\x72\x64\x66\x2d\x73\x79\x6e\x74\x61\x78\x2d\x6e\x73\x23\ +\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x73\x76\x67\x3d\x22\ +\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\ +\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x0a\x20\x20\x20\x78\ +\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\ +\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\ +\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x73\x6f\x64\x69\x70\ +\x6f\x64\x69\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x73\x6f\x64\x69\ +\x70\x6f\x64\x69\x2e\x73\x6f\x75\x72\x63\x65\x66\x6f\x72\x67\x65\ +\x2e\x6e\x65\x74\x2f\x44\x54\x44\x2f\x73\x6f\x64\x69\x70\x6f\x64\ +\x69\x2d\x30\x2e\x64\x74\x64\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\ +\x73\x3a\x69\x6e\x6b\x73\x63\x61\x70\x65\x3d\x22\x68\x74\x74\x70\ +\x3a\x2f\x2f\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\x65\x2e\ +\x6f\x72\x67\x2f\x6e\x61\x6d\x65\x73\x70\x61\x63\x65\x73\x2f\x69\ +\x6e\x6b\x73\x63\x61\x70\x65\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\ +\x63\x61\x70\x65\x3a\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x72\x63\x31\x20\x28\x30\x39\x39\x36\x30\x64\x36\x66\x30\x35\ +\x2c\x20\x32\x30\x32\x30\x2d\x30\x34\x2d\x30\x39\x29\x22\x0a\x20\ +\x20\x20\x73\x6f\x64\x69\x70\x6f\x64\x69\x3a\x64\x6f\x63\x6e\x61\ +\x6d\x65\x3d\x22\x61\x6c\x65\x72\x74\x2d\x6f\x63\x74\x61\x67\x6f\ +\x6e\x2d\x72\x65\x64\x2e\x73\x76\x67\x22\x0a\x20\x20\x20\x69\x64\ +\x3d\x22\x73\x76\x67\x38\x22\x0a\x20\x20\x20\x76\x65\x72\x73\x69\ +\x6f\x6e\x3d\x22\x31\x2e\x31\x22\x0a\x20\x20\x20\x63\x6c\x61\x73\ +\x73\x3d\x22\x66\x65\x61\x74\x68\x65\x72\x20\x66\x65\x61\x74\x68\ +\x65\x72\x2d\x61\x6c\x65\x72\x74\x2d\x6f\x63\x74\x61\x67\x6f\x6e\ +\x22\x0a\x20\x20\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\ +\x6a\x6f\x69\x6e\x3d\x22\x72\x6f\x75\x6e\x64\x22\x0a\x20\x20\x20\ +\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\x63\x61\x70\x3d\x22\ +\x72\x6f\x75\x6e\x64\x22\x0a\x20\x20\x20\x73\x74\x72\x6f\x6b\x65\ +\x2d\x77\x69\x64\x74\x68\x3d\x22\x32\x22\x0a\x20\x20\x20\x73\x74\ +\x72\x6f\x6b\x65\x3d\x22\x63\x75\x72\x72\x65\x6e\x74\x43\x6f\x6c\ +\x6f\x72\x22\x0a\x20\x20\x20\x66\x69\x6c\x6c\x3d\x22\x6e\x6f\x6e\ +\x65\x22\x0a\x20\x20\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\x30\ +\x20\x30\x20\x32\x34\x20\x32\x34\x22\x0a\x20\x20\x20\x68\x65\x69\ +\x67\x68\x74\x3d\x22\x32\x34\x22\x0a\x20\x20\x20\x77\x69\x64\x74\ +\x68\x3d\x22\x32\x34\x22\x3e\x0a\x20\x20\x3c\x6d\x65\x74\x61\x64\ +\x61\x74\x61\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x6d\x65\x74\ +\x61\x64\x61\x74\x61\x31\x34\x22\x3e\x0a\x20\x20\x20\x20\x3c\x72\ +\x64\x66\x3a\x52\x44\x46\x3e\x0a\x20\x20\x20\x20\x20\x20\x3c\x63\ +\x63\x3a\x57\x6f\x72\x6b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\ +\x72\x64\x66\x3a\x61\x62\x6f\x75\x74\x3d\x22\x22\x3e\x0a\x20\x20\ +\x20\x20\x20\x20\x20\x20\x3c\x64\x63\x3a\x66\x6f\x72\x6d\x61\x74\ +\x3e\x69\x6d\x61\x67\x65\x2f\x73\x76\x67\x2b\x78\x6d\x6c\x3c\x2f\ +\x64\x63\x3a\x66\x6f\x72\x6d\x61\x74\x3e\x0a\x20\x20\x20\x20\x20\ +\x20\x20\x20\x3c\x64\x63\x3a\x74\x79\x70\x65\x0a\x20\x20\x20\x20\ +\x20\x20\x20\x20\x20\x20\x20\x72\x64\x66\x3a\x72\x65\x73\x6f\x75\ +\x72\x63\x65\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x70\x75\x72\x6c\ +\x2e\x6f\x72\x67\x2f\x64\x63\x2f\x64\x63\x6d\x69\x74\x79\x70\x65\ +\x2f\x53\x74\x69\x6c\x6c\x49\x6d\x61\x67\x65\x22\x20\x2f\x3e\x0a\ +\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x63\x3a\x74\x69\x74\x6c\ +\x65\x3e\x3c\x2f\x64\x63\x3a\x74\x69\x74\x6c\x65\x3e\x0a\x20\x20\ +\x20\x20\x20\x20\x3c\x2f\x63\x63\x3a\x57\x6f\x72\x6b\x3e\x0a\x20\ +\x20\x20\x20\x3c\x2f\x72\x64\x66\x3a\x52\x44\x46\x3e\x0a\x20\x20\ +\x3c\x2f\x6d\x65\x74\x61\x64\x61\x74\x61\x3e\x0a\x20\x20\x3c\x64\ +\x65\x66\x73\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x64\x65\x66\ +\x73\x31\x32\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x73\x6f\x64\x69\x70\ +\x6f\x64\x69\x3a\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\x0a\x20\x20\ +\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x75\x72\x72\ +\x65\x6e\x74\x2d\x6c\x61\x79\x65\x72\x3d\x22\x73\x76\x67\x38\x22\ +\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\ +\x69\x6e\x64\x6f\x77\x2d\x6d\x61\x78\x69\x6d\x69\x7a\x65\x64\x3d\ +\x22\x30\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\ +\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x79\x3d\x22\x32\x38\x31\x22\ +\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\ +\x69\x6e\x64\x6f\x77\x2d\x78\x3d\x22\x31\x34\x33\x33\x22\x0a\x20\ +\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x79\x3d\ +\x22\x31\x32\x2e\x34\x30\x39\x37\x39\x37\x22\x0a\x20\x20\x20\x20\ +\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x78\x3d\x22\x31\x32\ +\x2e\x38\x32\x38\x32\x31\x36\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\ +\x6b\x73\x63\x61\x70\x65\x3a\x7a\x6f\x6f\x6d\x3d\x22\x32\x31\x22\ +\x0a\x20\x20\x20\x20\x20\x73\x68\x6f\x77\x67\x72\x69\x64\x3d\x22\ +\x66\x61\x6c\x73\x65\x22\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\ +\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\x31\x30\x22\x0a\x20\x20\x20\ +\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\ +\x77\x2d\x68\x65\x69\x67\x68\x74\x3d\x22\x39\x35\x39\x22\x0a\x20\ +\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\ +\x64\x6f\x77\x2d\x77\x69\x64\x74\x68\x3d\x22\x31\x35\x34\x32\x22\ +\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x70\ +\x61\x67\x65\x73\x68\x61\x64\x6f\x77\x3d\x22\x32\x22\x0a\x20\x20\ +\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x70\x61\x67\x65\ +\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\x30\x22\x0a\x20\x20\x20\x20\ +\x20\x67\x75\x69\x64\x65\x74\x6f\x6c\x65\x72\x61\x6e\x63\x65\x3d\ +\x22\x31\x30\x22\x0a\x20\x20\x20\x20\x20\x67\x72\x69\x64\x74\x6f\ +\x6c\x65\x72\x61\x6e\x63\x65\x3d\x22\x31\x30\x22\x0a\x20\x20\x20\ +\x20\x20\x6f\x62\x6a\x65\x63\x74\x74\x6f\x6c\x65\x72\x61\x6e\x63\ +\x65\x3d\x22\x31\x30\x22\x0a\x20\x20\x20\x20\x20\x62\x6f\x72\x64\ +\x65\x72\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\x31\x22\x0a\x20\x20\ +\x20\x20\x20\x62\x6f\x72\x64\x65\x72\x63\x6f\x6c\x6f\x72\x3d\x22\ +\x23\x36\x36\x36\x36\x36\x36\x22\x0a\x20\x20\x20\x20\x20\x70\x61\ +\x67\x65\x63\x6f\x6c\x6f\x72\x3d\x22\x23\x66\x66\x66\x66\x66\x66\ +\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x70\x6f\x6c\x79\x67\x6f\x6e\x0a\ +\x20\x20\x20\x20\x20\x73\x74\x79\x6c\x65\x3d\x22\x66\x69\x6c\x6c\ +\x3a\x23\x30\x30\x66\x66\x30\x30\x22\x0a\x20\x20\x20\x20\x20\x69\ +\x64\x3d\x22\x70\x6f\x6c\x79\x67\x6f\x6e\x32\x22\x0a\x20\x20\x20\ +\x20\x20\x70\x6f\x69\x6e\x74\x73\x3d\x22\x37\x2e\x38\x36\x20\x32\ +\x20\x31\x36\x2e\x31\x34\x20\x32\x20\x32\x32\x20\x37\x2e\x38\x36\ +\x20\x32\x32\x20\x31\x36\x2e\x31\x34\x20\x31\x36\x2e\x31\x34\x20\ +\x32\x32\x20\x37\x2e\x38\x36\x20\x32\x32\x20\x32\x20\x31\x36\x2e\ +\x31\x34\x20\x32\x20\x37\x2e\x38\x36\x20\x37\x2e\x38\x36\x20\x32\ +\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x6c\x69\x6e\x65\x0a\x20\x20\x20\ +\x20\x20\x69\x64\x3d\x22\x6c\x69\x6e\x65\x34\x22\x0a\x20\x20\x20\ +\x20\x20\x79\x32\x3d\x22\x31\x32\x22\x0a\x20\x20\x20\x20\x20\x78\ +\x32\x3d\x22\x31\x32\x22\x0a\x20\x20\x20\x20\x20\x79\x31\x3d\x22\ +\x38\x22\x0a\x20\x20\x20\x20\x20\x78\x31\x3d\x22\x31\x32\x22\x20\ +\x2f\x3e\x0a\x20\x20\x3c\x6c\x69\x6e\x65\x0a\x20\x20\x20\x20\x20\ +\x69\x64\x3d\x22\x6c\x69\x6e\x65\x36\x22\x0a\x20\x20\x20\x20\x20\ +\x79\x32\x3d\x22\x31\x36\x22\x0a\x20\x20\x20\x20\x20\x78\x32\x3d\ +\x22\x31\x32\x2e\x30\x31\x22\x0a\x20\x20\x20\x20\x20\x79\x31\x3d\ +\x22\x31\x36\x22\x0a\x20\x20\x20\x20\x20\x78\x31\x3d\x22\x31\x32\ +\x22\x20\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\x3e\x0a\ +\x00\x00\x01\x90\ +\x3c\ +\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ +\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ +\x30\x2f\x73\x76\x67\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x32\x34\ +\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x32\x34\x22\x20\x76\x69\ +\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\x30\x20\x32\x34\x20\x32\x34\ +\x22\x20\x66\x69\x6c\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\x20\x73\x74\ +\x72\x6f\x6b\x65\x3d\x22\x63\x75\x72\x72\x65\x6e\x74\x43\x6f\x6c\ +\x6f\x72\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\x68\ +\x3d\x22\x32\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\ +\x63\x61\x70\x3d\x22\x72\x6f\x75\x6e\x64\x22\x20\x73\x74\x72\x6f\ +\x6b\x65\x2d\x6c\x69\x6e\x65\x6a\x6f\x69\x6e\x3d\x22\x72\x6f\x75\ +\x6e\x64\x22\x20\x63\x6c\x61\x73\x73\x3d\x22\x66\x65\x61\x74\x68\ +\x65\x72\x20\x66\x65\x61\x74\x68\x65\x72\x2d\x72\x65\x66\x72\x65\ +\x73\x68\x2d\x63\x77\x22\x3e\x3c\x70\x6f\x6c\x79\x6c\x69\x6e\x65\ +\x20\x70\x6f\x69\x6e\x74\x73\x3d\x22\x32\x33\x20\x34\x20\x32\x33\ +\x20\x31\x30\x20\x31\x37\x20\x31\x30\x22\x3e\x3c\x2f\x70\x6f\x6c\ +\x79\x6c\x69\x6e\x65\x3e\x3c\x70\x6f\x6c\x79\x6c\x69\x6e\x65\x20\ +\x70\x6f\x69\x6e\x74\x73\x3d\x22\x31\x20\x32\x30\x20\x31\x20\x31\ +\x34\x20\x37\x20\x31\x34\x22\x3e\x3c\x2f\x70\x6f\x6c\x79\x6c\x69\ +\x6e\x65\x3e\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\x33\x2e\x35\ +\x31\x20\x39\x61\x39\x20\x39\x20\x30\x20\x30\x20\x31\x20\x31\x34\ +\x2e\x38\x35\x2d\x33\x2e\x33\x36\x4c\x32\x33\x20\x31\x30\x4d\x31\ +\x20\x31\x34\x6c\x34\x2e\x36\x34\x20\x34\x2e\x33\x36\x41\x39\x20\ +\x39\x20\x30\x20\x30\x20\x30\x20\x32\x30\x2e\x34\x39\x20\x31\x35\ +\x22\x3e\x3c\x2f\x70\x61\x74\x68\x3e\x3c\x2f\x73\x76\x67\x3e\ +\x00\x00\x01\x76\ +\x3c\ +\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ +\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ +\x30\x2f\x73\x76\x67\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x32\x34\ +\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x32\x34\x22\x20\x76\x69\ +\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\x30\x20\x32\x34\x20\x32\x34\ +\x22\x20\x66\x69\x6c\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\x20\x73\x74\ +\x72\x6f\x6b\x65\x3d\x22\x63\x75\x72\x72\x65\x6e\x74\x43\x6f\x6c\ +\x6f\x72\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\x68\ +\x3d\x22\x32\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\ +\x63\x61\x70\x3d\x22\x72\x6f\x75\x6e\x64\x22\x20\x73\x74\x72\x6f\ +\x6b\x65\x2d\x6c\x69\x6e\x65\x6a\x6f\x69\x6e\x3d\x22\x72\x6f\x75\ +\x6e\x64\x22\x20\x63\x6c\x61\x73\x73\x3d\x22\x66\x65\x61\x74\x68\ +\x65\x72\x20\x66\x65\x61\x74\x68\x65\x72\x2d\x64\x65\x6c\x65\x74\ +\x65\x22\x3e\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\x32\x31\x20\ +\x34\x48\x38\x6c\x2d\x37\x20\x38\x20\x37\x20\x38\x68\x31\x33\x61\ +\x32\x20\x32\x20\x30\x20\x30\x20\x30\x20\x32\x2d\x32\x56\x36\x61\ +\x32\x20\x32\x20\x30\x20\x30\x20\x30\x2d\x32\x2d\x32\x7a\x22\x3e\ +\x3c\x2f\x70\x61\x74\x68\x3e\x3c\x6c\x69\x6e\x65\x20\x78\x31\x3d\ +\x22\x31\x38\x22\x20\x79\x31\x3d\x22\x39\x22\x20\x78\x32\x3d\x22\ +\x31\x32\x22\x20\x79\x32\x3d\x22\x31\x35\x22\x3e\x3c\x2f\x6c\x69\ +\x6e\x65\x3e\x3c\x6c\x69\x6e\x65\x20\x78\x31\x3d\x22\x31\x32\x22\ +\x20\x79\x31\x3d\x22\x39\x22\x20\x78\x32\x3d\x22\x31\x38\x22\x20\ +\x79\x32\x3d\x22\x31\x35\x22\x3e\x3c\x2f\x6c\x69\x6e\x65\x3e\x3c\ +\x2f\x73\x76\x67\x3e\ +\x00\x00\x07\x9d\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\x20\x73\x74\x61\x6e\x64\x61\x6c\x6f\x6e\x65\x3d\x22\ +\x6e\x6f\x22\x3f\x3e\x0a\x3c\x73\x76\x67\x0a\x20\x20\x20\x78\x6d\ +\x6c\x6e\x73\x3a\x64\x63\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x70\ +\x75\x72\x6c\x2e\x6f\x72\x67\x2f\x64\x63\x2f\x65\x6c\x65\x6d\x65\ +\x6e\x74\x73\x2f\x31\x2e\x31\x2f\x22\x0a\x20\x20\x20\x78\x6d\x6c\ +\x6e\x73\x3a\x63\x63\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x63\x72\ +\x65\x61\x74\x69\x76\x65\x63\x6f\x6d\x6d\x6f\x6e\x73\x2e\x6f\x72\ +\x67\x2f\x6e\x73\x23\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\ +\x72\x64\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\ +\x77\x33\x2e\x6f\x72\x67\x2f\x31\x39\x39\x39\x2f\x30\x32\x2f\x32\ +\x32\x2d\x72\x64\x66\x2d\x73\x79\x6e\x74\x61\x78\x2d\x6e\x73\x23\ +\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x73\x76\x67\x3d\x22\ +\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\ +\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x0a\x20\x20\x20\x78\ +\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\ +\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\ +\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x73\x6f\x64\x69\x70\ +\x6f\x64\x69\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x73\x6f\x64\x69\ +\x70\x6f\x64\x69\x2e\x73\x6f\x75\x72\x63\x65\x66\x6f\x72\x67\x65\ +\x2e\x6e\x65\x74\x2f\x44\x54\x44\x2f\x73\x6f\x64\x69\x70\x6f\x64\ +\x69\x2d\x30\x2e\x64\x74\x64\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\ +\x73\x3a\x69\x6e\x6b\x73\x63\x61\x70\x65\x3d\x22\x68\x74\x74\x70\ +\x3a\x2f\x2f\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\x65\x2e\ +\x6f\x72\x67\x2f\x6e\x61\x6d\x65\x73\x70\x61\x63\x65\x73\x2f\x69\ +\x6e\x6b\x73\x63\x61\x70\x65\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\ +\x63\x61\x70\x65\x3a\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x72\x63\x31\x20\x28\x30\x39\x39\x36\x30\x64\x36\x66\x30\x35\ +\x2c\x20\x32\x30\x32\x30\x2d\x30\x34\x2d\x30\x39\x29\x22\x0a\x20\ +\x20\x20\x73\x6f\x64\x69\x70\x6f\x64\x69\x3a\x64\x6f\x63\x6e\x61\ +\x6d\x65\x3d\x22\x61\x6c\x65\x72\x74\x2d\x6f\x63\x74\x61\x67\x6f\ +\x6e\x2d\x72\x65\x64\x2e\x73\x76\x67\x22\x0a\x20\x20\x20\x69\x64\ +\x3d\x22\x73\x76\x67\x38\x22\x0a\x20\x20\x20\x76\x65\x72\x73\x69\ +\x6f\x6e\x3d\x22\x31\x2e\x31\x22\x0a\x20\x20\x20\x63\x6c\x61\x73\ +\x73\x3d\x22\x66\x65\x61\x74\x68\x65\x72\x20\x66\x65\x61\x74\x68\ +\x65\x72\x2d\x61\x6c\x65\x72\x74\x2d\x6f\x63\x74\x61\x67\x6f\x6e\ +\x22\x0a\x20\x20\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\ +\x6a\x6f\x69\x6e\x3d\x22\x72\x6f\x75\x6e\x64\x22\x0a\x20\x20\x20\ +\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\x63\x61\x70\x3d\x22\ +\x72\x6f\x75\x6e\x64\x22\x0a\x20\x20\x20\x73\x74\x72\x6f\x6b\x65\ +\x2d\x77\x69\x64\x74\x68\x3d\x22\x32\x22\x0a\x20\x20\x20\x73\x74\ +\x72\x6f\x6b\x65\x3d\x22\x63\x75\x72\x72\x65\x6e\x74\x43\x6f\x6c\ +\x6f\x72\x22\x0a\x20\x20\x20\x66\x69\x6c\x6c\x3d\x22\x6e\x6f\x6e\ +\x65\x22\x0a\x20\x20\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\x30\ +\x20\x30\x20\x32\x34\x20\x32\x34\x22\x0a\x20\x20\x20\x68\x65\x69\ +\x67\x68\x74\x3d\x22\x32\x34\x22\x0a\x20\x20\x20\x77\x69\x64\x74\ +\x68\x3d\x22\x32\x34\x22\x3e\x0a\x20\x20\x3c\x6d\x65\x74\x61\x64\ +\x61\x74\x61\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x6d\x65\x74\ +\x61\x64\x61\x74\x61\x31\x34\x22\x3e\x0a\x20\x20\x20\x20\x3c\x72\ +\x64\x66\x3a\x52\x44\x46\x3e\x0a\x20\x20\x20\x20\x20\x20\x3c\x63\ +\x63\x3a\x57\x6f\x72\x6b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\ +\x72\x64\x66\x3a\x61\x62\x6f\x75\x74\x3d\x22\x22\x3e\x0a\x20\x20\ +\x20\x20\x20\x20\x20\x20\x3c\x64\x63\x3a\x66\x6f\x72\x6d\x61\x74\ +\x3e\x69\x6d\x61\x67\x65\x2f\x73\x76\x67\x2b\x78\x6d\x6c\x3c\x2f\ +\x64\x63\x3a\x66\x6f\x72\x6d\x61\x74\x3e\x0a\x20\x20\x20\x20\x20\ +\x20\x20\x20\x3c\x64\x63\x3a\x74\x79\x70\x65\x0a\x20\x20\x20\x20\ +\x20\x20\x20\x20\x20\x20\x20\x72\x64\x66\x3a\x72\x65\x73\x6f\x75\ +\x72\x63\x65\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x70\x75\x72\x6c\ +\x2e\x6f\x72\x67\x2f\x64\x63\x2f\x64\x63\x6d\x69\x74\x79\x70\x65\ +\x2f\x53\x74\x69\x6c\x6c\x49\x6d\x61\x67\x65\x22\x20\x2f\x3e\x0a\ +\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x63\x3a\x74\x69\x74\x6c\ +\x65\x3e\x3c\x2f\x64\x63\x3a\x74\x69\x74\x6c\x65\x3e\x0a\x20\x20\ +\x20\x20\x20\x20\x3c\x2f\x63\x63\x3a\x57\x6f\x72\x6b\x3e\x0a\x20\ +\x20\x20\x20\x3c\x2f\x72\x64\x66\x3a\x52\x44\x46\x3e\x0a\x20\x20\ +\x3c\x2f\x6d\x65\x74\x61\x64\x61\x74\x61\x3e\x0a\x20\x20\x3c\x64\ +\x65\x66\x73\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x64\x65\x66\ +\x73\x31\x32\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x73\x6f\x64\x69\x70\ +\x6f\x64\x69\x3a\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\x0a\x20\x20\ +\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x75\x72\x72\ +\x65\x6e\x74\x2d\x6c\x61\x79\x65\x72\x3d\x22\x73\x76\x67\x38\x22\ +\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\ +\x69\x6e\x64\x6f\x77\x2d\x6d\x61\x78\x69\x6d\x69\x7a\x65\x64\x3d\ +\x22\x30\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\ +\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x79\x3d\x22\x32\x38\x31\x22\ +\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\ +\x69\x6e\x64\x6f\x77\x2d\x78\x3d\x22\x31\x34\x33\x33\x22\x0a\x20\ +\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x79\x3d\ +\x22\x31\x32\x2e\x34\x30\x39\x37\x39\x37\x22\x0a\x20\x20\x20\x20\ +\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x78\x3d\x22\x31\x32\ +\x2e\x38\x32\x38\x32\x31\x36\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\ +\x6b\x73\x63\x61\x70\x65\x3a\x7a\x6f\x6f\x6d\x3d\x22\x32\x31\x22\ +\x0a\x20\x20\x20\x20\x20\x73\x68\x6f\x77\x67\x72\x69\x64\x3d\x22\ +\x66\x61\x6c\x73\x65\x22\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\ +\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\x31\x30\x22\x0a\x20\x20\x20\ +\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\ +\x77\x2d\x68\x65\x69\x67\x68\x74\x3d\x22\x39\x35\x39\x22\x0a\x20\ +\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\ +\x64\x6f\x77\x2d\x77\x69\x64\x74\x68\x3d\x22\x31\x35\x34\x32\x22\ +\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x70\ +\x61\x67\x65\x73\x68\x61\x64\x6f\x77\x3d\x22\x32\x22\x0a\x20\x20\ +\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x70\x61\x67\x65\ +\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\x30\x22\x0a\x20\x20\x20\x20\ +\x20\x67\x75\x69\x64\x65\x74\x6f\x6c\x65\x72\x61\x6e\x63\x65\x3d\ +\x22\x31\x30\x22\x0a\x20\x20\x20\x20\x20\x67\x72\x69\x64\x74\x6f\ +\x6c\x65\x72\x61\x6e\x63\x65\x3d\x22\x31\x30\x22\x0a\x20\x20\x20\ +\x20\x20\x6f\x62\x6a\x65\x63\x74\x74\x6f\x6c\x65\x72\x61\x6e\x63\ +\x65\x3d\x22\x31\x30\x22\x0a\x20\x20\x20\x20\x20\x62\x6f\x72\x64\ +\x65\x72\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\x31\x22\x0a\x20\x20\ +\x20\x20\x20\x62\x6f\x72\x64\x65\x72\x63\x6f\x6c\x6f\x72\x3d\x22\ +\x23\x36\x36\x36\x36\x36\x36\x22\x0a\x20\x20\x20\x20\x20\x70\x61\ +\x67\x65\x63\x6f\x6c\x6f\x72\x3d\x22\x23\x66\x66\x66\x66\x66\x66\ +\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x70\x6f\x6c\x79\x67\x6f\x6e\x0a\ +\x20\x20\x20\x20\x20\x73\x74\x79\x6c\x65\x3d\x22\x66\x69\x6c\x6c\ +\x3a\x23\x66\x66\x30\x30\x30\x30\x22\x0a\x20\x20\x20\x20\x20\x69\ +\x64\x3d\x22\x70\x6f\x6c\x79\x67\x6f\x6e\x32\x22\x0a\x20\x20\x20\ +\x20\x20\x70\x6f\x69\x6e\x74\x73\x3d\x22\x37\x2e\x38\x36\x20\x32\ +\x20\x31\x36\x2e\x31\x34\x20\x32\x20\x32\x32\x20\x37\x2e\x38\x36\ +\x20\x32\x32\x20\x31\x36\x2e\x31\x34\x20\x31\x36\x2e\x31\x34\x20\ +\x32\x32\x20\x37\x2e\x38\x36\x20\x32\x32\x20\x32\x20\x31\x36\x2e\ +\x31\x34\x20\x32\x20\x37\x2e\x38\x36\x20\x37\x2e\x38\x36\x20\x32\ +\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x6c\x69\x6e\x65\x0a\x20\x20\x20\ +\x20\x20\x69\x64\x3d\x22\x6c\x69\x6e\x65\x34\x22\x0a\x20\x20\x20\ +\x20\x20\x79\x32\x3d\x22\x31\x32\x22\x0a\x20\x20\x20\x20\x20\x78\ +\x32\x3d\x22\x31\x32\x22\x0a\x20\x20\x20\x20\x20\x79\x31\x3d\x22\ +\x38\x22\x0a\x20\x20\x20\x20\x20\x78\x31\x3d\x22\x31\x32\x22\x20\ +\x2f\x3e\x0a\x20\x20\x3c\x6c\x69\x6e\x65\x0a\x20\x20\x20\x20\x20\ +\x69\x64\x3d\x22\x6c\x69\x6e\x65\x36\x22\x0a\x20\x20\x20\x20\x20\ +\x79\x32\x3d\x22\x31\x36\x22\x0a\x20\x20\x20\x20\x20\x78\x32\x3d\ +\x22\x31\x32\x2e\x30\x31\x22\x0a\x20\x20\x20\x20\x20\x79\x31\x3d\ +\x22\x31\x36\x22\x0a\x20\x20\x20\x20\x20\x78\x31\x3d\x22\x31\x32\ +\x22\x20\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\x3e\x0a\ +\x00\x00\x06\x4c\ +\x3c\ +\x73\x76\x67\x20\x77\x69\x64\x74\x68\x3d\x22\x38\x30\x22\x20\x68\ +\x65\x69\x67\x68\x74\x3d\x22\x38\x30\x22\x20\x78\x6d\x6c\x6e\x73\ +\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\ +\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x20\x66\x69\ +\x6c\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\x3e\x0a\x20\x3c\x67\x3e\x0a\ +\x20\x20\x3c\x74\x69\x74\x6c\x65\x3e\x4c\x61\x79\x65\x72\x20\x31\ +\x3c\x2f\x74\x69\x74\x6c\x65\x3e\x0a\x20\x20\x3c\x70\x61\x74\x68\ +\x20\x66\x69\x6c\x6c\x2d\x72\x75\x6c\x65\x3d\x22\x65\x76\x65\x6e\ +\x6f\x64\x64\x22\x20\x63\x6c\x69\x70\x2d\x72\x75\x6c\x65\x3d\x22\ +\x65\x76\x65\x6e\x6f\x64\x64\x22\x20\x64\x3d\x22\x6d\x33\x33\x2e\ +\x39\x32\x35\x36\x2c\x39\x2e\x36\x39\x33\x32\x34\x63\x2d\x30\x2e\ +\x37\x36\x30\x38\x2c\x30\x20\x2d\x31\x2e\x34\x38\x30\x32\x2c\x30\ +\x2e\x33\x34\x36\x34\x33\x20\x2d\x31\x2e\x39\x35\x34\x35\x2c\x30\ +\x2e\x39\x34\x31\x32\x33\x6c\x2d\x35\x2e\x33\x32\x31\x2c\x36\x2e\ +\x36\x37\x32\x33\x6c\x2d\x31\x31\x2e\x36\x35\x30\x31\x2c\x30\x63\ +\x2d\x31\x2e\x33\x38\x30\x37\x2c\x30\x20\x2d\x32\x2e\x35\x2c\x31\ +\x2e\x31\x31\x39\x33\x20\x2d\x32\x2e\x35\x2c\x32\x2e\x35\x63\x30\ +\x2c\x31\x2e\x33\x38\x30\x37\x20\x31\x2e\x31\x31\x39\x33\x2c\x32\ +\x2e\x35\x20\x32\x2e\x35\x2c\x32\x2e\x35\x6c\x31\x2e\x35\x2c\x30\ +\x6c\x30\x2c\x34\x31\x2e\x35\x63\x30\x2c\x33\x2e\x35\x38\x39\x39\ +\x20\x32\x2e\x39\x31\x30\x31\x2c\x36\x2e\x35\x20\x36\x2e\x35\x2c\ +\x36\x2e\x35\x6c\x33\x34\x2c\x30\x63\x33\x2e\x35\x38\x39\x38\x2c\ +\x30\x20\x36\x2e\x35\x2c\x2d\x32\x2e\x39\x31\x30\x31\x20\x36\x2e\ +\x35\x2c\x2d\x36\x2e\x35\x6c\x30\x2c\x2d\x34\x31\x2e\x35\x6c\x31\ +\x2e\x35\x2c\x30\x63\x31\x2e\x33\x38\x30\x37\x2c\x30\x20\x32\x2e\ +\x35\x2c\x2d\x31\x2e\x31\x31\x39\x33\x20\x32\x2e\x35\x2c\x2d\x32\ +\x2e\x35\x63\x30\x2c\x2d\x31\x2e\x33\x38\x30\x37\x20\x2d\x31\x2e\ +\x31\x31\x39\x33\x2c\x2d\x32\x2e\x35\x20\x2d\x32\x2e\x35\x2c\x2d\ +\x32\x2e\x35\x6c\x2d\x31\x31\x2e\x36\x35\x30\x31\x2c\x30\x6c\x2d\ +\x35\x2e\x33\x32\x30\x39\x2c\x2d\x36\x2e\x36\x37\x32\x32\x63\x2d\ +\x30\x2e\x34\x37\x34\x34\x2c\x2d\x30\x2e\x35\x39\x34\x39\x20\x2d\ +\x31\x2e\x31\x39\x33\x38\x2c\x2d\x30\x2e\x39\x34\x31\x33\x33\x20\ +\x2d\x31\x2e\x39\x35\x34\x36\x2c\x2d\x30\x2e\x39\x34\x31\x33\x33\ +\x6c\x2d\x31\x32\x2e\x31\x34\x38\x38\x2c\x30\x7a\x6d\x2d\x30\x2e\ +\x39\x32\x35\x36\x2c\x31\x37\x2e\x36\x31\x33\x35\x33\x63\x31\x2e\ +\x33\x38\x30\x37\x2c\x30\x20\x32\x2e\x35\x2c\x31\x2e\x31\x31\x39\ +\x33\x20\x32\x2e\x35\x2c\x32\x2e\x35\x6c\x30\x2c\x32\x38\x63\x30\ +\x2c\x31\x2e\x33\x38\x30\x37\x20\x2d\x31\x2e\x31\x31\x39\x33\x2c\ +\x32\x2e\x35\x20\x2d\x32\x2e\x35\x2c\x32\x2e\x35\x63\x2d\x31\x2e\ +\x33\x38\x30\x37\x2c\x30\x20\x2d\x32\x2e\x35\x2c\x2d\x31\x2e\x31\ +\x31\x39\x33\x20\x2d\x32\x2e\x35\x2c\x2d\x32\x2e\x35\x6c\x30\x2c\ +\x2d\x32\x38\x63\x30\x2c\x2d\x31\x2e\x33\x38\x30\x37\x20\x31\x2e\ +\x31\x31\x39\x33\x2c\x2d\x32\x2e\x35\x20\x32\x2e\x35\x2c\x2d\x32\ +\x2e\x35\x7a\x6d\x31\x36\x2e\x35\x2c\x32\x2e\x35\x63\x30\x2c\x2d\ +\x31\x2e\x33\x38\x30\x37\x20\x2d\x31\x2e\x31\x31\x39\x33\x2c\x2d\ +\x32\x2e\x35\x20\x2d\x32\x2e\x35\x2c\x2d\x32\x2e\x35\x63\x2d\x31\ +\x2e\x33\x38\x30\x37\x2c\x30\x20\x2d\x32\x2e\x35\x2c\x31\x2e\x31\ +\x31\x39\x33\x20\x2d\x32\x2e\x35\x2c\x32\x2e\x35\x6c\x30\x2c\x32\ +\x38\x63\x30\x2c\x31\x2e\x33\x38\x30\x37\x20\x31\x2e\x31\x31\x39\ +\x33\x2c\x32\x2e\x35\x20\x32\x2e\x35\x2c\x32\x2e\x35\x63\x31\x2e\ +\x33\x38\x30\x37\x2c\x30\x20\x32\x2e\x35\x2c\x2d\x31\x2e\x31\x31\ +\x39\x33\x20\x32\x2e\x35\x2c\x2d\x32\x2e\x35\x6c\x30\x2c\x2d\x32\ +\x38\x7a\x6d\x2d\x32\x2e\x35\x34\x36\x34\x2c\x2d\x31\x32\x2e\x35\ +\x30\x31\x34\x6c\x2d\x32\x2e\x30\x38\x33\x32\x2c\x2d\x32\x2e\x36\ +\x31\x32\x31\x6c\x2d\x39\x2e\x37\x34\x30\x38\x2c\x30\x6c\x2d\x32\ +\x2e\x30\x38\x33\x32\x2c\x32\x2e\x36\x31\x32\x31\x6c\x31\x33\x2e\ +\x39\x30\x37\x32\x2c\x30\x7a\x22\x20\x66\x69\x6c\x6c\x3d\x22\x23\ +\x43\x32\x43\x43\x44\x45\x22\x20\x69\x64\x3d\x22\x73\x76\x67\x5f\ +\x31\x22\x2f\x3e\x0a\x20\x20\x3c\x70\x61\x74\x68\x20\x66\x69\x6c\ +\x6c\x3d\x22\x23\x66\x66\x30\x30\x30\x30\x22\x20\x64\x3d\x22\x6d\ +\x37\x2e\x33\x33\x38\x33\x2c\x33\x39\x2e\x39\x39\x39\x39\x34\x6c\ +\x30\x2c\x30\x63\x30\x2c\x2d\x31\x38\x2e\x30\x38\x35\x33\x35\x20\ +\x31\x34\x2e\x36\x32\x33\x31\x36\x2c\x2d\x33\x32\x2e\x37\x34\x36\ +\x35\x31\x20\x33\x32\x2e\x36\x36\x31\x36\x37\x2c\x2d\x33\x32\x2e\ +\x37\x34\x36\x35\x31\x6c\x30\x2c\x30\x63\x38\x2e\x36\x36\x32\x35\ +\x33\x2c\x30\x20\x31\x36\x2e\x39\x37\x30\x32\x35\x2c\x33\x2e\x34\ +\x35\x30\x30\x38\x20\x32\x33\x2e\x30\x39\x35\x33\x32\x2c\x39\x2e\ +\x35\x39\x31\x32\x36\x63\x36\x2e\x31\x32\x35\x33\x31\x2c\x36\x2e\ +\x31\x34\x31\x31\x39\x20\x39\x2e\x35\x36\x36\x34\x32\x2c\x31\x34\ +\x2e\x34\x37\x30\x34\x34\x20\x39\x2e\x35\x36\x36\x34\x32\x2c\x32\ +\x33\x2e\x31\x35\x35\x32\x34\x6c\x30\x2c\x30\x63\x30\x2c\x31\x38\ +\x2e\x30\x38\x35\x35\x36\x20\x2d\x31\x34\x2e\x36\x32\x33\x30\x35\ +\x2c\x33\x32\x2e\x37\x34\x36\x36\x33\x20\x2d\x33\x32\x2e\x36\x36\ +\x31\x37\x33\x2c\x33\x32\x2e\x37\x34\x36\x36\x33\x6c\x30\x2c\x30\ +\x63\x2d\x31\x38\x2e\x30\x33\x38\x35\x31\x2c\x30\x20\x2d\x33\x32\ +\x2e\x36\x36\x31\x36\x37\x2c\x2d\x31\x34\x2e\x36\x36\x31\x30\x36\ +\x20\x2d\x33\x32\x2e\x36\x36\x31\x36\x37\x2c\x2d\x33\x32\x2e\x37\ +\x34\x36\x36\x33\x63\x30\x2c\x30\x20\x30\x2c\x30\x20\x30\x2c\x30\ +\x6c\x2d\x30\x2e\x30\x30\x30\x30\x31\x2c\x30\x7a\x6d\x35\x32\x2e\ +\x37\x34\x31\x32\x31\x2c\x31\x34\x2e\x36\x34\x39\x31\x36\x6c\x30\ +\x2c\x30\x63\x37\x2e\x31\x39\x31\x30\x32\x2c\x2d\x39\x2e\x39\x30\ +\x38\x33\x34\x20\x36\x2e\x31\x32\x32\x30\x39\x2c\x2d\x32\x33\x2e\ +\x35\x38\x39\x34\x38\x20\x2d\x32\x2e\x35\x32\x30\x31\x35\x2c\x2d\ +\x33\x32\x2e\x32\x35\x34\x30\x37\x63\x2d\x38\x2e\x36\x34\x32\x32\ +\x35\x2c\x2d\x38\x2e\x36\x36\x34\x36\x36\x20\x2d\x32\x32\x2e\x32\ +\x38\x37\x39\x33\x2c\x2d\x39\x2e\x37\x33\x36\x33\x37\x20\x2d\x33\ +\x32\x2e\x31\x37\x30\x33\x33\x2c\x2d\x32\x2e\x35\x32\x36\x35\x38\ +\x6c\x33\x34\x2e\x36\x39\x30\x34\x37\x2c\x33\x34\x2e\x37\x38\x30\ +\x36\x35\x6c\x30\x2e\x30\x30\x30\x30\x31\x2c\x30\x7a\x6d\x2d\x34\ +\x30\x2e\x31\x35\x38\x38\x39\x2c\x2d\x32\x39\x2e\x32\x39\x38\x30\ +\x31\x63\x2d\x37\x2e\x31\x39\x31\x30\x37\x2c\x39\x2e\x39\x30\x38\ +\x32\x38\x20\x2d\x36\x2e\x31\x32\x32\x31\x37\x2c\x32\x33\x2e\x35\ +\x38\x39\x34\x33\x20\x32\x2e\x35\x32\x30\x30\x32\x2c\x33\x32\x2e\ +\x32\x35\x33\x39\x63\x38\x2e\x36\x34\x32\x31\x37\x2c\x38\x2e\x36\ +\x36\x34\x37\x31\x20\x32\x32\x2e\x32\x38\x37\x38\x35\x2c\x39\x2e\ +\x37\x33\x36\x34\x31\x20\x33\x32\x2e\x31\x37\x30\x32\x35\x2c\x32\ +\x2e\x35\x32\x36\x37\x6c\x2d\x33\x34\x2e\x36\x39\x30\x32\x38\x2c\ +\x2d\x33\x34\x2e\x37\x38\x30\x35\x39\x6c\x30\x2c\x30\x6c\x30\x2e\ +\x30\x30\x30\x30\x31\x2c\x2d\x30\x2e\x30\x30\x30\x30\x31\x7a\x22\ +\x20\x69\x64\x3d\x22\x73\x76\x67\x5f\x33\x22\x2f\x3e\x0a\x20\x3c\ +\x2f\x67\x3e\x0a\x0a\x3c\x2f\x73\x76\x67\x3e\ +\x00\x00\x09\x97\ +\x3c\ +\x73\x76\x67\x20\x66\x69\x6c\x6c\x3d\x22\x23\x30\x30\x30\x30\x30\ +\x30\x22\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\ +\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\ +\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\x30\ +\x20\x30\x20\x35\x30\x20\x35\x30\x22\x20\x77\x69\x64\x74\x68\x3d\ +\x22\x35\x30\x70\x78\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x35\ +\x30\x70\x78\x22\x3e\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\x20\ +\x32\x35\x20\x32\x20\x43\x20\x32\x30\x2e\x39\x34\x31\x34\x30\x36\ +\x20\x32\x20\x31\x38\x2e\x31\x38\x37\x35\x20\x32\x2e\x39\x36\x38\ +\x37\x35\x20\x31\x36\x2e\x34\x33\x37\x35\x20\x34\x2e\x33\x37\x35\ +\x20\x43\x20\x31\x34\x2e\x36\x38\x37\x35\x20\x35\x2e\x37\x38\x31\ +\x32\x35\x20\x31\x34\x20\x37\x2e\x35\x38\x39\x38\x34\x34\x20\x31\ +\x34\x20\x39\x2e\x30\x39\x33\x37\x35\x20\x4c\x20\x31\x34\x20\x31\ +\x34\x20\x4c\x20\x32\x34\x20\x31\x34\x20\x4c\x20\x32\x34\x20\x31\ +\x35\x20\x4c\x20\x39\x2e\x30\x39\x33\x37\x35\x20\x31\x35\x20\x43\ +\x20\x37\x2e\x32\x36\x35\x36\x32\x35\x20\x31\x35\x20\x35\x2e\x34\ +\x31\x30\x31\x35\x36\x20\x31\x35\x2e\x37\x39\x32\x39\x36\x39\x20\ +\x34\x2e\x30\x39\x33\x37\x35\x20\x31\x37\x2e\x34\x36\x38\x37\x35\ +\x20\x43\x20\x32\x2e\x37\x37\x37\x33\x34\x34\x20\x31\x39\x2e\x31\ +\x34\x34\x35\x33\x31\x20\x32\x20\x32\x31\x2e\x36\x34\x34\x35\x33\ +\x31\x20\x32\x20\x32\x35\x20\x43\x20\x32\x20\x32\x38\x2e\x33\x35\ +\x35\x34\x36\x39\x20\x32\x2e\x37\x37\x37\x33\x34\x34\x20\x33\x30\ +\x2e\x38\x35\x35\x34\x36\x39\x20\x34\x2e\x30\x39\x33\x37\x35\x20\ +\x33\x32\x2e\x35\x33\x31\x32\x35\x20\x43\x20\x35\x2e\x34\x31\x30\ +\x31\x35\x36\x20\x33\x34\x2e\x32\x30\x37\x30\x33\x31\x20\x37\x2e\ +\x32\x36\x35\x36\x32\x35\x20\x33\x35\x20\x39\x2e\x30\x39\x33\x37\ +\x35\x20\x33\x35\x20\x4c\x20\x31\x34\x20\x33\x35\x20\x4c\x20\x31\ +\x34\x20\x34\x30\x2e\x39\x30\x36\x32\x35\x20\x43\x20\x31\x34\x20\ +\x34\x32\x2e\x34\x31\x30\x31\x35\x36\x20\x31\x34\x2e\x36\x38\x37\ +\x35\x20\x34\x34\x2e\x32\x31\x38\x37\x35\x20\x31\x36\x2e\x34\x33\ +\x37\x35\x20\x34\x35\x2e\x36\x32\x35\x20\x43\x20\x31\x38\x2e\x31\ +\x38\x37\x35\x20\x34\x37\x2e\x30\x33\x31\x32\x35\x20\x32\x30\x2e\ +\x39\x34\x31\x34\x30\x36\x20\x34\x38\x20\x32\x35\x20\x34\x38\x20\ +\x43\x20\x32\x39\x2e\x30\x35\x38\x35\x39\x34\x20\x34\x38\x20\x33\ +\x31\x2e\x38\x31\x32\x35\x20\x34\x37\x2e\x30\x33\x31\x32\x35\x20\ +\x33\x33\x2e\x35\x36\x32\x35\x20\x34\x35\x2e\x36\x32\x35\x20\x43\ +\x20\x33\x35\x2e\x33\x31\x32\x35\x20\x34\x34\x2e\x32\x31\x38\x37\ +\x35\x20\x33\x36\x20\x34\x32\x2e\x34\x31\x30\x31\x35\x36\x20\x33\ +\x36\x20\x34\x30\x2e\x39\x30\x36\x32\x35\x20\x4c\x20\x33\x36\x20\ +\x33\x36\x20\x4c\x20\x32\x36\x20\x33\x36\x20\x4c\x20\x32\x36\x20\ +\x33\x35\x20\x4c\x20\x34\x30\x2e\x39\x30\x36\x32\x35\x20\x33\x35\ +\x20\x43\x20\x34\x32\x2e\x37\x33\x34\x33\x37\x35\x20\x33\x35\x20\ +\x34\x34\x2e\x35\x38\x39\x38\x34\x34\x20\x33\x34\x2e\x32\x30\x37\ +\x30\x33\x31\x20\x34\x35\x2e\x39\x30\x36\x32\x35\x20\x33\x32\x2e\ +\x35\x33\x31\x32\x35\x20\x43\x20\x34\x37\x2e\x32\x32\x32\x36\x35\ +\x36\x20\x33\x30\x2e\x38\x35\x35\x34\x36\x39\x20\x34\x38\x20\x32\ +\x38\x2e\x33\x35\x35\x34\x36\x39\x20\x34\x38\x20\x32\x35\x20\x43\ +\x20\x34\x38\x20\x32\x31\x2e\x36\x34\x34\x35\x33\x31\x20\x34\x37\ +\x2e\x32\x32\x32\x36\x35\x36\x20\x31\x39\x2e\x31\x34\x34\x35\x33\ +\x31\x20\x34\x35\x2e\x39\x30\x36\x32\x35\x20\x31\x37\x2e\x34\x36\ +\x38\x37\x35\x20\x43\x20\x34\x34\x2e\x35\x38\x39\x38\x34\x34\x20\ +\x31\x35\x2e\x37\x39\x32\x39\x36\x39\x20\x34\x32\x2e\x37\x33\x34\ +\x33\x37\x35\x20\x31\x35\x20\x34\x30\x2e\x39\x30\x36\x32\x35\x20\ +\x31\x35\x20\x4c\x20\x33\x36\x20\x31\x35\x20\x4c\x20\x33\x36\x20\ +\x39\x2e\x30\x39\x33\x37\x35\x20\x43\x20\x33\x36\x20\x37\x2e\x35\ +\x35\x30\x37\x38\x31\x20\x33\x35\x2e\x33\x31\x36\x34\x30\x36\x20\ +\x35\x2e\x37\x33\x38\x32\x38\x31\x20\x33\x33\x2e\x35\x36\x32\x35\ +\x20\x34\x2e\x33\x34\x33\x37\x35\x20\x43\x20\x33\x31\x2e\x38\x30\ +\x38\x35\x39\x34\x20\x32\x2e\x39\x34\x39\x32\x31\x39\x20\x32\x39\ +\x2e\x30\x35\x34\x36\x38\x38\x20\x32\x20\x32\x35\x20\x32\x20\x5a\ +\x20\x4d\x20\x32\x35\x20\x34\x20\x43\x20\x32\x38\x2e\x37\x34\x36\ +\x30\x39\x34\x20\x34\x20\x33\x31\x2e\x30\x31\x35\x36\x32\x35\x20\ +\x34\x2e\x38\x37\x35\x20\x33\x32\x2e\x33\x31\x32\x35\x20\x35\x2e\ +\x39\x30\x36\x32\x35\x20\x43\x20\x33\x33\x2e\x36\x30\x39\x33\x37\ +\x35\x20\x36\x2e\x39\x33\x37\x35\x20\x33\x34\x20\x38\x2e\x31\x33\ +\x36\x37\x31\x39\x20\x33\x34\x20\x39\x2e\x30\x39\x33\x37\x35\x20\ +\x4c\x20\x33\x34\x20\x32\x31\x20\x43\x20\x33\x34\x20\x32\x32\x2e\ +\x36\x35\x36\x32\x35\x20\x33\x32\x2e\x36\x35\x36\x32\x35\x20\x32\ +\x34\x20\x33\x31\x20\x32\x34\x20\x4c\x20\x31\x39\x20\x32\x34\x20\ +\x43\x20\x31\x36\x2e\x39\x34\x31\x34\x30\x36\x20\x32\x34\x20\x31\ +\x35\x2e\x31\x36\x37\x39\x36\x39\x20\x32\x35\x2e\x32\x36\x39\x35\ +\x33\x31\x20\x31\x34\x2e\x34\x30\x36\x32\x35\x20\x32\x37\x2e\x30\ +\x36\x32\x35\x20\x43\x20\x31\x34\x2e\x32\x37\x37\x33\x34\x34\x20\ +\x32\x37\x2e\x33\x35\x39\x33\x37\x35\x20\x31\x34\x2e\x31\x36\x30\ +\x31\x35\x36\x20\x32\x37\x2e\x36\x37\x35\x37\x38\x31\x20\x31\x34\ +\x2e\x30\x39\x33\x37\x35\x20\x32\x38\x20\x43\x20\x31\x34\x2e\x30\ +\x32\x37\x33\x34\x34\x20\x32\x38\x2e\x33\x32\x34\x32\x31\x39\x20\ +\x31\x34\x20\x32\x38\x2e\x36\x35\x36\x32\x35\x20\x31\x34\x20\x32\ +\x39\x20\x4c\x20\x31\x34\x20\x33\x33\x20\x4c\x20\x39\x2e\x30\x39\ +\x33\x37\x35\x20\x33\x33\x20\x43\x20\x37\x2e\x38\x32\x34\x32\x31\ +\x39\x20\x33\x33\x20\x36\x2e\x36\x34\x38\x34\x33\x38\x20\x33\x32\ +\x2e\x35\x30\x33\x39\x30\x36\x20\x35\x2e\x36\x38\x37\x35\x20\x33\ +\x31\x2e\x32\x38\x31\x32\x35\x20\x43\x20\x34\x2e\x37\x32\x36\x35\ +\x36\x33\x20\x33\x30\x2e\x30\x35\x38\x35\x39\x34\x20\x34\x20\x32\ +\x38\x2e\x30\x34\x32\x39\x36\x39\x20\x34\x20\x32\x35\x20\x43\x20\ +\x34\x20\x32\x31\x2e\x39\x35\x37\x30\x33\x31\x20\x34\x2e\x37\x32\ +\x36\x35\x36\x33\x20\x31\x39\x2e\x39\x34\x31\x34\x30\x36\x20\x35\ +\x2e\x36\x38\x37\x35\x20\x31\x38\x2e\x37\x31\x38\x37\x35\x20\x43\ +\x20\x36\x2e\x36\x34\x38\x34\x33\x38\x20\x31\x37\x2e\x34\x39\x36\ +\x30\x39\x34\x20\x37\x2e\x38\x32\x34\x32\x31\x39\x20\x31\x37\x20\ +\x39\x2e\x30\x39\x33\x37\x35\x20\x31\x37\x20\x4c\x20\x32\x36\x20\ +\x31\x37\x20\x4c\x20\x32\x36\x20\x31\x32\x20\x4c\x20\x31\x36\x20\ +\x31\x32\x20\x4c\x20\x31\x36\x20\x39\x2e\x30\x39\x33\x37\x35\x20\ +\x43\x20\x31\x36\x20\x38\x2e\x31\x39\x39\x32\x31\x39\x20\x31\x36\ +\x2e\x33\x38\x36\x37\x31\x39\x20\x36\x2e\x39\x38\x30\x34\x36\x39\ +\x20\x31\x37\x2e\x36\x38\x37\x35\x20\x35\x2e\x39\x33\x37\x35\x20\ +\x43\x20\x31\x38\x2e\x39\x38\x38\x32\x38\x31\x20\x34\x2e\x38\x39\ +\x34\x35\x33\x31\x20\x32\x31\x2e\x32\x35\x37\x38\x31\x33\x20\x34\ +\x20\x32\x35\x20\x34\x20\x5a\x20\x4d\x20\x32\x30\x20\x37\x20\x43\ +\x20\x31\x38\x2e\x38\x39\x38\x34\x33\x38\x20\x37\x20\x31\x38\x20\ +\x37\x2e\x38\x39\x38\x34\x33\x38\x20\x31\x38\x20\x39\x20\x43\x20\ +\x31\x38\x20\x31\x30\x2e\x31\x30\x31\x35\x36\x33\x20\x31\x38\x2e\ +\x38\x39\x38\x34\x33\x38\x20\x31\x31\x20\x32\x30\x20\x31\x31\x20\ +\x43\x20\x32\x31\x2e\x31\x30\x31\x35\x36\x33\x20\x31\x31\x20\x32\ +\x32\x20\x31\x30\x2e\x31\x30\x31\x35\x36\x33\x20\x32\x32\x20\x39\ +\x20\x43\x20\x32\x32\x20\x37\x2e\x38\x39\x38\x34\x33\x38\x20\x32\ +\x31\x2e\x31\x30\x31\x35\x36\x33\x20\x37\x20\x32\x30\x20\x37\x20\ +\x5a\x20\x4d\x20\x33\x36\x20\x31\x37\x20\x4c\x20\x34\x30\x2e\x39\ +\x30\x36\x32\x35\x20\x31\x37\x20\x43\x20\x34\x32\x2e\x31\x37\x35\ +\x37\x38\x31\x20\x31\x37\x20\x34\x33\x2e\x33\x35\x31\x35\x36\x33\ +\x20\x31\x37\x2e\x34\x39\x36\x30\x39\x34\x20\x34\x34\x2e\x33\x31\ +\x32\x35\x20\x31\x38\x2e\x37\x31\x38\x37\x35\x20\x43\x20\x34\x35\ +\x2e\x32\x37\x33\x34\x33\x38\x20\x31\x39\x2e\x39\x34\x31\x34\x30\ +\x36\x20\x34\x36\x20\x32\x31\x2e\x39\x35\x37\x30\x33\x31\x20\x34\ +\x36\x20\x32\x35\x20\x43\x20\x34\x36\x20\x32\x38\x2e\x30\x34\x32\ +\x39\x36\x39\x20\x34\x35\x2e\x32\x37\x33\x34\x33\x38\x20\x33\x30\ +\x2e\x30\x35\x38\x35\x39\x34\x20\x34\x34\x2e\x33\x31\x32\x35\x20\ +\x33\x31\x2e\x32\x38\x31\x32\x35\x20\x43\x20\x34\x33\x2e\x33\x35\ +\x31\x35\x36\x33\x20\x33\x32\x2e\x35\x30\x33\x39\x30\x36\x20\x34\ +\x32\x2e\x31\x37\x35\x37\x38\x31\x20\x33\x33\x20\x34\x30\x2e\x39\ +\x30\x36\x32\x35\x20\x33\x33\x20\x4c\x20\x32\x34\x20\x33\x33\x20\ +\x4c\x20\x32\x34\x20\x33\x38\x20\x4c\x20\x33\x34\x20\x33\x38\x20\ +\x4c\x20\x33\x34\x20\x34\x30\x2e\x39\x30\x36\x32\x35\x20\x43\x20\ +\x33\x34\x20\x34\x31\x2e\x38\x30\x30\x37\x38\x31\x20\x33\x33\x2e\ +\x36\x31\x33\x32\x38\x31\x20\x34\x33\x2e\x30\x31\x39\x35\x33\x31\ +\x20\x33\x32\x2e\x33\x31\x32\x35\x20\x34\x34\x2e\x30\x36\x32\x35\ +\x20\x43\x20\x33\x31\x2e\x30\x31\x31\x37\x31\x39\x20\x34\x35\x2e\ +\x31\x30\x35\x34\x36\x39\x20\x32\x38\x2e\x37\x34\x32\x31\x38\x38\ +\x20\x34\x36\x20\x32\x35\x20\x34\x36\x20\x43\x20\x32\x31\x2e\x32\ +\x35\x37\x38\x31\x33\x20\x34\x36\x20\x31\x38\x2e\x39\x38\x38\x32\ +\x38\x31\x20\x34\x35\x2e\x31\x30\x35\x34\x36\x39\x20\x31\x37\x2e\ +\x36\x38\x37\x35\x20\x34\x34\x2e\x30\x36\x32\x35\x20\x43\x20\x31\ +\x36\x2e\x33\x38\x36\x37\x31\x39\x20\x34\x33\x2e\x30\x31\x39\x35\ +\x33\x31\x20\x31\x36\x20\x34\x31\x2e\x38\x30\x30\x37\x38\x31\x20\ +\x31\x36\x20\x34\x30\x2e\x39\x30\x36\x32\x35\x20\x4c\x20\x31\x36\ +\x20\x32\x39\x20\x43\x20\x31\x36\x20\x32\x38\x2e\x37\x39\x32\x39\ +\x36\x39\x20\x31\x36\x2e\x30\x32\x33\x34\x33\x38\x20\x32\x38\x2e\ +\x36\x30\x31\x35\x36\x33\x20\x31\x36\x2e\x30\x36\x32\x35\x20\x32\ +\x38\x2e\x34\x30\x36\x32\x35\x20\x43\x20\x31\x36\x2e\x33\x34\x33\ +\x37\x35\x20\x32\x37\x2e\x30\x33\x39\x30\x36\x33\x20\x31\x37\x2e\ +\x35\x35\x30\x37\x38\x31\x20\x32\x36\x20\x31\x39\x20\x32\x36\x20\ +\x4c\x20\x33\x31\x20\x32\x36\x20\x43\x20\x33\x33\x2e\x37\x34\x36\ +\x30\x39\x34\x20\x32\x36\x20\x33\x36\x20\x32\x33\x2e\x37\x34\x36\ +\x30\x39\x34\x20\x33\x36\x20\x32\x31\x20\x5a\x20\x4d\x20\x33\x30\ +\x20\x33\x39\x20\x43\x20\x32\x38\x2e\x38\x39\x38\x34\x33\x38\x20\ +\x33\x39\x20\x32\x38\x20\x33\x39\x2e\x38\x39\x38\x34\x33\x38\x20\ +\x32\x38\x20\x34\x31\x20\x43\x20\x32\x38\x20\x34\x32\x2e\x31\x30\ +\x31\x35\x36\x33\x20\x32\x38\x2e\x38\x39\x38\x34\x33\x38\x20\x34\ +\x33\x20\x33\x30\x20\x34\x33\x20\x43\x20\x33\x31\x2e\x31\x30\x31\ +\x35\x36\x33\x20\x34\x33\x20\x33\x32\x20\x34\x32\x2e\x31\x30\x31\ +\x35\x36\x33\x20\x33\x32\x20\x34\x31\x20\x43\x20\x33\x32\x20\x33\ +\x39\x2e\x38\x39\x38\x34\x33\x38\x20\x33\x31\x2e\x31\x30\x31\x35\ +\x36\x33\x20\x33\x39\x20\x33\x30\x20\x33\x39\x20\x5a\x22\x2f\x3e\ +\x3c\x2f\x73\x76\x67\x3e\ +" + +qt_resource_name = b"\ +\x00\x09\ +\x00\x28\xbf\x23\ +\x00\x73\ +\x00\x74\x00\x79\x00\x6c\x00\x65\x00\x2e\x00\x63\x00\x73\x00\x73\ +\x00\x05\ +\x00\x6f\xa6\x53\ +\x00\x69\ +\x00\x63\x00\x6f\x00\x6e\x00\x73\ +\x00\x0f\ +\x00\x50\xd7\x47\ +\x00\x70\ +\x00\x6c\x00\x75\x00\x73\x00\x2d\x00\x73\x00\x71\x00\x75\x00\x61\x00\x72\x00\x65\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x0c\ +\x02\x33\x2a\x87\ +\x00\x6e\ +\x00\x6f\x00\x2d\x00\x61\x00\x6c\x00\x65\x00\x72\x00\x74\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x10\ +\x03\xdc\xdd\x87\ +\x00\x73\ +\x00\x74\x00\x61\x00\x72\x00\x2d\x00\x63\x00\x72\x00\x6f\x00\x73\x00\x73\x00\x65\x00\x64\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x08\ +\x05\x77\x54\xa7\ +\x00\x6c\ +\x00\x6f\x00\x61\x00\x64\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x08\ +\x05\xa8\x57\x87\ +\x00\x63\ +\x00\x6f\x00\x64\x00\x65\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x0a\ +\x08\x4a\xc4\x07\ +\x00\x65\ +\x00\x78\x00\x70\x00\x61\x00\x6e\x00\x64\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x09\ +\x08\x9b\xad\xc7\ +\x00\x74\ +\x00\x72\x00\x61\x00\x73\x00\x68\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x08\ +\x08\xc8\x55\xe7\ +\x00\x73\ +\x00\x61\x00\x76\x00\x65\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x07\ +\x09\xc7\x5a\x27\ +\x00\x73\ +\x00\x65\x00\x74\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x08\ +\x0a\x85\x55\x87\ +\x00\x73\ +\x00\x74\x00\x61\x00\x72\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x0a\ +\x0a\xc8\xf6\x87\ +\x00\x66\ +\x00\x6f\x00\x6c\x00\x64\x00\x65\x00\x72\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x0c\ +\x0a\xdc\x3f\xc7\ +\x00\x63\ +\x00\x6f\x00\x6c\x00\x6c\x00\x61\x00\x70\x00\x73\x00\x65\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x0f\ +\x0b\x14\x80\xa7\ +\x00\x67\ +\x00\x72\x00\x65\x00\x65\x00\x6e\x00\x2d\x00\x61\x00\x6c\x00\x65\x00\x72\x00\x74\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x0b\ +\x0c\x6a\x21\xc7\ +\x00\x72\ +\x00\x65\x00\x66\x00\x72\x00\x65\x00\x73\x00\x68\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x0a\ +\x0c\xad\x02\x87\ +\x00\x64\ +\x00\x65\x00\x6c\x00\x65\x00\x74\x00\x65\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x0d\ +\x0d\xf9\x2b\x67\ +\x00\x72\ +\x00\x65\x00\x64\x00\x2d\x00\x61\x00\x6c\x00\x65\x00\x72\x00\x74\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x11\ +\x0e\x2c\x55\xe7\ +\x00\x74\ +\x00\x72\x00\x61\x00\x73\x00\x68\x00\x2d\x00\x63\x00\x72\x00\x6f\x00\x73\x00\x73\x00\x65\x00\x64\x00\x2e\x00\x73\x00\x76\x00\x67\ +\ +\x00\x0a\ +\x0f\x6e\x5b\x87\ +\x00\x70\ +\x00\x79\x00\x74\x00\x68\x00\x6f\x00\x6e\x00\x2e\x00\x73\x00\x76\x00\x67\ +" + +qt_resource_struct_v1 = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +\x00\x00\x00\x18\x00\x02\x00\x00\x00\x12\x00\x00\x00\x03\ +\x00\x00\x00\x28\x00\x00\x00\x00\x00\x01\x00\x00\x00\x04\ +\x00\x00\x00\x4c\x00\x00\x00\x00\x00\x01\x00\x00\x01\x7d\ +\x00\x00\x00\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x03\x21\ +\x00\x00\x00\x90\x00\x00\x00\x00\x00\x01\x00\x00\x0a\xfe\ +\x00\x00\x00\xa6\x00\x00\x00\x00\x00\x01\x00\x00\x0c\x6e\ +\x00\x00\x00\xbc\x00\x01\x00\x00\x00\x01\x00\x00\x0d\xa5\ +\x00\x00\x00\xd6\x00\x00\x00\x00\x00\x01\x00\x00\x11\x51\ +\x00\x00\x00\xee\x00\x00\x00\x00\x00\x01\x00\x00\x14\xf0\ +\x00\x00\x01\x04\x00\x00\x00\x00\x00\x01\x00\x00\x16\x7c\ +\x00\x00\x01\x18\x00\x00\x00\x00\x00\x01\x00\x00\x17\xf0\ +\x00\x00\x01\x2e\x00\x00\x00\x00\x00\x01\x00\x00\x1d\xae\ +\x00\x00\x01\x48\x00\x00\x00\x00\x00\x01\x00\x00\x22\x74\ +\x00\x00\x01\x66\x00\x00\x00\x00\x00\x01\x00\x00\x2c\x07\ +\x00\x00\x01\x8a\x00\x00\x00\x00\x00\x01\x00\x00\x33\xa8\ +\x00\x00\x01\xa6\x00\x00\x00\x00\x00\x01\x00\x00\x35\x3c\ +\x00\x00\x01\xc0\x00\x00\x00\x00\x00\x01\x00\x00\x36\xb6\ +\x00\x00\x01\xe0\x00\x00\x00\x00\x00\x01\x00\x00\x3e\x57\ +\x00\x00\x02\x08\x00\x00\x00\x00\x00\x01\x00\x00\x44\xa7\ +" + +qt_resource_struct_v2 = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +\x00\x00\x01\x95\xe8\xaa\xa9\xd5\ +\x00\x00\x00\x18\x00\x02\x00\x00\x00\x12\x00\x00\x00\x03\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x28\x00\x00\x00\x00\x00\x01\x00\x00\x00\x04\ +\x00\x00\x01\x95\xe8\xaa\xa9\xd4\ +\x00\x00\x00\x4c\x00\x00\x00\x00\x00\x01\x00\x00\x01\x7d\ +\x00\x00\x01\x95\xe8\xaa\xa9\xd3\ +\x00\x00\x00\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x03\x21\ +\x00\x00\x01\x95\xe8\xaa\xa9\xd4\ +\x00\x00\x00\x90\x00\x00\x00\x00\x00\x01\x00\x00\x0a\xfe\ +\x00\x00\x01\x95\xe8\xaa\xa9\xd4\ +\x00\x00\x00\xa6\x00\x00\x00\x00\x00\x01\x00\x00\x0c\x6e\ +\x00\x00\x01\x95\xe8\xaa\xa9\xd3\ +\x00\x00\x00\xbc\x00\x01\x00\x00\x00\x01\x00\x00\x0d\xa5\ +\x00\x00\x01\x95\xe8\xaa\xa9\xd3\ +\x00\x00\x00\xd6\x00\x00\x00\x00\x00\x01\x00\x00\x11\x51\ +\x00\x00\x01\x95\xe8\xaa\xa9\xd5\ +\x00\x00\x00\xee\x00\x00\x00\x00\x00\x01\x00\x00\x14\xf0\ +\x00\x00\x01\x95\xe8\xaa\xa9\xd4\ +\x00\x00\x01\x04\x00\x00\x00\x00\x00\x01\x00\x00\x16\x7c\ +\x00\x00\x01\x95\xe8\xaa\xa9\xd4\ +\x00\x00\x01\x18\x00\x00\x00\x00\x00\x01\x00\x00\x17\xf0\ +\x00\x00\x01\x95\xe8\xaa\xa9\xd5\ +\x00\x00\x01\x2e\x00\x00\x00\x00\x00\x01\x00\x00\x1d\xae\ +\x00\x00\x01\x9a\x4b\xc3\x1d\x94\ +\x00\x00\x01\x48\x00\x00\x00\x00\x00\x01\x00\x00\x22\x74\ +\x00\x00\x01\x95\xe8\xaa\xa9\xd3\ +\x00\x00\x01\x66\x00\x00\x00\x00\x00\x01\x00\x00\x2c\x07\ +\x00\x00\x01\x95\xe8\xaa\xa9\xd2\ +\x00\x00\x01\x8a\x00\x00\x00\x00\x00\x01\x00\x00\x33\xa8\ +\x00\x00\x01\x95\xe8\xaa\xa9\xd4\ +\x00\x00\x01\xa6\x00\x00\x00\x00\x00\x01\x00\x00\x35\x3c\ +\x00\x00\x01\x95\xe8\xaa\xa9\xd3\ +\x00\x00\x01\xc0\x00\x00\x00\x00\x00\x01\x00\x00\x36\xb6\ +\x00\x00\x01\x95\xe8\xaa\xa9\xd3\ +\x00\x00\x01\xe0\x00\x00\x00\x00\x00\x01\x00\x00\x3e\x57\ +\x00\x00\x01\x95\xe8\xaa\xa9\xd5\ +\x00\x00\x02\x08\x00\x00\x00\x00\x00\x01\x00\x00\x44\xa7\ +\x00\x00\x01\x95\xe8\xaa\xa9\xd4\ +" + +qt_version = [int(v) for v in QtCore.qVersion().split('.')] +if qt_version < [5, 8, 0]: + rcc_version = 1 + qt_resource_struct = qt_resource_struct_v1 +else: + rcc_version = 2 + qt_resource_struct = qt_resource_struct_v2 + +def qInitResources(): + QtCore.qRegisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) + +def qCleanupResources(): + QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) + +qInitResources() diff --git a/instrumentserver/resource.qrc b/src/instrumentserver/resource.qrc similarity index 100% rename from instrumentserver/resource.qrc rename to src/instrumentserver/resource.qrc diff --git a/instrumentserver/resource/icons/alert-octagon-green.svg b/src/instrumentserver/resource/icons/alert-octagon-green.svg similarity index 100% rename from instrumentserver/resource/icons/alert-octagon-green.svg rename to src/instrumentserver/resource/icons/alert-octagon-green.svg diff --git a/instrumentserver/resource/icons/alert-octagon-red.svg b/src/instrumentserver/resource/icons/alert-octagon-red.svg similarity index 100% rename from instrumentserver/resource/icons/alert-octagon-red.svg rename to src/instrumentserver/resource/icons/alert-octagon-red.svg diff --git a/instrumentserver/resource/icons/alert-octagon.svg b/src/instrumentserver/resource/icons/alert-octagon.svg similarity index 100% rename from instrumentserver/resource/icons/alert-octagon.svg rename to src/instrumentserver/resource/icons/alert-octagon.svg diff --git a/instrumentserver/resource/icons/client_app_icon.svg b/src/instrumentserver/resource/icons/client_app_icon.svg similarity index 100% rename from instrumentserver/resource/icons/client_app_icon.svg rename to src/instrumentserver/resource/icons/client_app_icon.svg diff --git a/instrumentserver/resource/icons/code.svg b/src/instrumentserver/resource/icons/code.svg similarity index 100% rename from instrumentserver/resource/icons/code.svg rename to src/instrumentserver/resource/icons/code.svg diff --git a/instrumentserver/resource/icons/collapse.svg b/src/instrumentserver/resource/icons/collapse.svg similarity index 100% rename from instrumentserver/resource/icons/collapse.svg rename to src/instrumentserver/resource/icons/collapse.svg diff --git a/instrumentserver/resource/icons/delete.svg b/src/instrumentserver/resource/icons/delete.svg similarity index 100% rename from instrumentserver/resource/icons/delete.svg rename to src/instrumentserver/resource/icons/delete.svg diff --git a/instrumentserver/resource/icons/expand.svg b/src/instrumentserver/resource/icons/expand.svg similarity index 100% rename from instrumentserver/resource/icons/expand.svg rename to src/instrumentserver/resource/icons/expand.svg diff --git a/instrumentserver/resource/icons/folder.svg b/src/instrumentserver/resource/icons/folder.svg similarity index 100% rename from instrumentserver/resource/icons/folder.svg rename to src/instrumentserver/resource/icons/folder.svg diff --git a/instrumentserver/resource/icons/load.svg b/src/instrumentserver/resource/icons/load.svg similarity index 100% rename from instrumentserver/resource/icons/load.svg rename to src/instrumentserver/resource/icons/load.svg diff --git a/instrumentserver/resource/icons/plus-square.svg b/src/instrumentserver/resource/icons/plus-square.svg similarity index 100% rename from instrumentserver/resource/icons/plus-square.svg rename to src/instrumentserver/resource/icons/plus-square.svg diff --git a/instrumentserver/resource/icons/python.svg b/src/instrumentserver/resource/icons/python.svg similarity index 100% rename from instrumentserver/resource/icons/python.svg rename to src/instrumentserver/resource/icons/python.svg diff --git a/instrumentserver/resource/icons/refresh.svg b/src/instrumentserver/resource/icons/refresh.svg similarity index 100% rename from instrumentserver/resource/icons/refresh.svg rename to src/instrumentserver/resource/icons/refresh.svg diff --git a/instrumentserver/resource/icons/save.svg b/src/instrumentserver/resource/icons/save.svg similarity index 100% rename from instrumentserver/resource/icons/save.svg rename to src/instrumentserver/resource/icons/save.svg diff --git a/instrumentserver/resource/icons/server_app_icon.svg b/src/instrumentserver/resource/icons/server_app_icon.svg similarity index 100% rename from instrumentserver/resource/icons/server_app_icon.svg rename to src/instrumentserver/resource/icons/server_app_icon.svg diff --git a/instrumentserver/resource/icons/set.svg b/src/instrumentserver/resource/icons/set.svg similarity index 100% rename from instrumentserver/resource/icons/set.svg rename to src/instrumentserver/resource/icons/set.svg diff --git a/instrumentserver/resource/icons/star-crossed.svg b/src/instrumentserver/resource/icons/star-crossed.svg similarity index 100% rename from instrumentserver/resource/icons/star-crossed.svg rename to src/instrumentserver/resource/icons/star-crossed.svg diff --git a/instrumentserver/resource/icons/star.svg b/src/instrumentserver/resource/icons/star.svg similarity index 100% rename from instrumentserver/resource/icons/star.svg rename to src/instrumentserver/resource/icons/star.svg diff --git a/instrumentserver/resource/icons/trash-crossed.svg b/src/instrumentserver/resource/icons/trash-crossed.svg similarity index 100% rename from instrumentserver/resource/icons/trash-crossed.svg rename to src/instrumentserver/resource/icons/trash-crossed.svg diff --git a/instrumentserver/resource/icons/trash.svg b/src/instrumentserver/resource/icons/trash.svg similarity index 100% rename from instrumentserver/resource/icons/trash.svg rename to src/instrumentserver/resource/icons/trash.svg diff --git a/instrumentserver/resource/style.css b/src/instrumentserver/resource/style.css similarity index 100% rename from instrumentserver/resource/style.css rename to src/instrumentserver/resource/style.css diff --git a/instrumentserver/schemas/instruction_dict.json b/src/instrumentserver/schemas/instruction_dict.json similarity index 100% rename from instrumentserver/schemas/instruction_dict.json rename to src/instrumentserver/schemas/instruction_dict.json diff --git a/instrumentserver/schemas/parameters.json b/src/instrumentserver/schemas/parameters.json similarity index 100% rename from instrumentserver/schemas/parameters.json rename to src/instrumentserver/schemas/parameters.json diff --git a/instrumentserver/serialize.py b/src/instrumentserver/serialize.py similarity index 100% rename from instrumentserver/serialize.py rename to src/instrumentserver/serialize.py diff --git a/instrumentserver/server/__init__.py b/src/instrumentserver/server/__init__.py similarity index 100% rename from instrumentserver/server/__init__.py rename to src/instrumentserver/server/__init__.py diff --git a/instrumentserver/server/application.py b/src/instrumentserver/server/application.py similarity index 100% rename from instrumentserver/server/application.py rename to src/instrumentserver/server/application.py diff --git a/instrumentserver/server/core.py b/src/instrumentserver/server/core.py similarity index 100% rename from instrumentserver/server/core.py rename to src/instrumentserver/server/core.py diff --git a/instrumentserver/server/pollingWorker.py b/src/instrumentserver/server/pollingWorker.py similarity index 100% rename from instrumentserver/server/pollingWorker.py rename to src/instrumentserver/server/pollingWorker.py diff --git a/instrumentserver/testing/__init__.py b/src/instrumentserver/testing/__init__.py similarity index 100% rename from instrumentserver/testing/__init__.py rename to src/instrumentserver/testing/__init__.py diff --git a/instrumentserver/testing/create_instrument.py b/src/instrumentserver/testing/create_instrument.py similarity index 100% rename from instrumentserver/testing/create_instrument.py rename to src/instrumentserver/testing/create_instrument.py diff --git a/instrumentserver/testing/dummy_instruments/__init__.py b/src/instrumentserver/testing/dummy_instruments/__init__.py similarity index 100% rename from instrumentserver/testing/dummy_instruments/__init__.py rename to src/instrumentserver/testing/dummy_instruments/__init__.py diff --git a/instrumentserver/testing/dummy_instruments/generic.py b/src/instrumentserver/testing/dummy_instruments/generic.py similarity index 100% rename from instrumentserver/testing/dummy_instruments/generic.py rename to src/instrumentserver/testing/dummy_instruments/generic.py diff --git a/instrumentserver/testing/dummy_instruments/rf.py b/src/instrumentserver/testing/dummy_instruments/rf.py similarity index 100% rename from instrumentserver/testing/dummy_instruments/rf.py rename to src/instrumentserver/testing/dummy_instruments/rf.py diff --git a/test/notebooks/Autoupdate.ipynb b/test/notebooks/Autoupdate.ipynb index a6e9328..3e3f937 100644 --- a/test/notebooks/Autoupdate.ipynb +++ b/test/notebooks/Autoupdate.ipynb @@ -137,17 +137,17 @@ "evalue": "'ProxyInstrumentModule' object and its delegates have no attribute 'test'", "output_type": "error", "traceback": [ - "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[1;31mAttributeError\u001b[0m Traceback (most recent call last)", - "\u001b[1;32mc:\\users\\msmt\\documents\\github\\instrumentserver\\instrumentserver\\client\\proxy.py\u001b[0m in \u001b[0;36m__getattr__\u001b[1;34m(self, item)\u001b[0m\n\u001b[0;32m 326\u001b[0m \u001b[1;32mtry\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 327\u001b[1;33m \u001b[1;32mreturn\u001b[0m \u001b[0msuper\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m__getattr__\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mitem\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 328\u001b[0m \u001b[1;32mexcept\u001b[0m \u001b[0mException\u001b[0m \u001b[1;32mas\u001b[0m \u001b[0me\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32m~\\Miniconda3\\envs\\qcodes\\lib\\site-packages\\qcodes\\utils\\helpers.py\u001b[0m in \u001b[0;36m__getattr__\u001b[1;34m(self, key)\u001b[0m\n\u001b[0;32m 405\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 406\u001b[1;33m raise AttributeError(\n\u001b[0m\u001b[0;32m 407\u001b[0m \"'{}' object and its delegates have no attribute '{}'\".format(\n", - "\u001b[1;31mAttributeError\u001b[0m: 'ProxyInstrumentModule' object and its delegates have no attribute 'test'", + "\u001B[1;31m---------------------------------------------------------------------------\u001B[0m", + "\u001B[1;31mAttributeError\u001B[0m Traceback (most recent call last)", + "\u001B[1;32mc:\\users\\msmt\\documents\\github\\instrumentserver\\instrumentserver\\client\\proxy.py\u001B[0m in \u001B[0;36m__getattr__\u001B[1;34m(self, item)\u001B[0m\n\u001B[0;32m 326\u001B[0m \u001B[1;32mtry\u001B[0m\u001B[1;33m:\u001B[0m\u001B[1;33m\u001B[0m\u001B[1;33m\u001B[0m\u001B[0m\n\u001B[1;32m--> 327\u001B[1;33m \u001B[1;32mreturn\u001B[0m \u001B[0msuper\u001B[0m\u001B[1;33m(\u001B[0m\u001B[1;33m)\u001B[0m\u001B[1;33m.\u001B[0m\u001B[0m__getattr__\u001B[0m\u001B[1;33m(\u001B[0m\u001B[0mitem\u001B[0m\u001B[1;33m)\u001B[0m\u001B[1;33m\u001B[0m\u001B[1;33m\u001B[0m\u001B[0m\n\u001B[0m\u001B[0;32m 328\u001B[0m \u001B[1;32mexcept\u001B[0m \u001B[0mException\u001B[0m \u001B[1;32mas\u001B[0m \u001B[0me\u001B[0m\u001B[1;33m:\u001B[0m\u001B[1;33m\u001B[0m\u001B[1;33m\u001B[0m\u001B[0m\n", + "\u001B[1;32m~\\Miniconda3\\envs\\qcodes\\lib\\site-packages\\qcodes\\utils\\helpers.py\u001B[0m in \u001B[0;36m__getattr__\u001B[1;34m(self, key)\u001B[0m\n\u001B[0;32m 405\u001B[0m \u001B[1;33m\u001B[0m\u001B[0m\n\u001B[1;32m--> 406\u001B[1;33m raise AttributeError(\n\u001B[0m\u001B[0;32m 407\u001B[0m \"'{}' object and its delegates have no attribute '{}'\".format(\n", + "\u001B[1;31mAttributeError\u001B[0m: 'ProxyInstrumentModule' object and its delegates have no attribute 'test'", "\nDuring handling of the above exception, another exception occurred:\n", - "\u001b[1;31mAttributeError\u001b[0m Traceback (most recent call last)", - "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[0mparams\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mqubit\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mtest\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", - "\u001b[1;32mc:\\users\\msmt\\documents\\github\\instrumentserver\\instrumentserver\\client\\proxy.py\u001b[0m in \u001b[0;36m__getattr__\u001b[1;34m(self, item)\u001b[0m\n\u001b[0;32m 328\u001b[0m \u001b[1;32mexcept\u001b[0m \u001b[0mException\u001b[0m \u001b[1;32mas\u001b[0m \u001b[0me\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 329\u001b[0m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34mf\"{type(e)}: {e.args}\"\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 330\u001b[1;33m \u001b[1;32mreturn\u001b[0m \u001b[0msuper\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m__getattr__\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mitem\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 331\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 332\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32m~\\Miniconda3\\envs\\qcodes\\lib\\site-packages\\qcodes\\utils\\helpers.py\u001b[0m in \u001b[0;36m__getattr__\u001b[1;34m(self, key)\u001b[0m\n\u001b[0;32m 404\u001b[0m \u001b[1;32mpass\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 405\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 406\u001b[1;33m raise AttributeError(\n\u001b[0m\u001b[0;32m 407\u001b[0m \"'{}' object and its delegates have no attribute '{}'\".format(\n\u001b[0;32m 408\u001b[0m self.__class__.__name__, key))\n", - "\u001b[1;31mAttributeError\u001b[0m: 'ProxyInstrumentModule' object and its delegates have no attribute 'test'" + "\u001B[1;31mAttributeError\u001B[0m Traceback (most recent call last)", + "\u001B[1;32m\u001B[0m in \u001B[0;36m\u001B[1;34m\u001B[0m\n\u001B[1;32m----> 1\u001B[1;33m \u001B[0mparams\u001B[0m\u001B[1;33m.\u001B[0m\u001B[0mqubit\u001B[0m\u001B[1;33m.\u001B[0m\u001B[0mtest\u001B[0m\u001B[1;33m(\u001B[0m\u001B[1;33m)\u001B[0m\u001B[1;33m\u001B[0m\u001B[1;33m\u001B[0m\u001B[0m\n\u001B[0m", + "\u001B[1;32mc:\\users\\msmt\\documents\\github\\instrumentserver\\instrumentserver\\client\\proxy.py\u001B[0m in \u001B[0;36m__getattr__\u001B[1;34m(self, item)\u001B[0m\n\u001B[0;32m 328\u001B[0m \u001B[1;32mexcept\u001B[0m \u001B[0mException\u001B[0m \u001B[1;32mas\u001B[0m \u001B[0me\u001B[0m\u001B[1;33m:\u001B[0m\u001B[1;33m\u001B[0m\u001B[1;33m\u001B[0m\u001B[0m\n\u001B[0;32m 329\u001B[0m \u001B[0mprint\u001B[0m\u001B[1;33m(\u001B[0m\u001B[1;34mf\"{type(e)}: {e.args}\"\u001B[0m\u001B[1;33m)\u001B[0m\u001B[1;33m\u001B[0m\u001B[1;33m\u001B[0m\u001B[0m\n\u001B[1;32m--> 330\u001B[1;33m \u001B[1;32mreturn\u001B[0m \u001B[0msuper\u001B[0m\u001B[1;33m(\u001B[0m\u001B[1;33m)\u001B[0m\u001B[1;33m.\u001B[0m\u001B[0m__getattr__\u001B[0m\u001B[1;33m(\u001B[0m\u001B[0mitem\u001B[0m\u001B[1;33m)\u001B[0m\u001B[1;33m\u001B[0m\u001B[1;33m\u001B[0m\u001B[0m\n\u001B[0m\u001B[0;32m 331\u001B[0m \u001B[1;33m\u001B[0m\u001B[0m\n\u001B[0;32m 332\u001B[0m \u001B[1;33m\u001B[0m\u001B[0m\n", + "\u001B[1;32m~\\Miniconda3\\envs\\qcodes\\lib\\site-packages\\qcodes\\utils\\helpers.py\u001B[0m in \u001B[0;36m__getattr__\u001B[1;34m(self, key)\u001B[0m\n\u001B[0;32m 404\u001B[0m \u001B[1;32mpass\u001B[0m\u001B[1;33m\u001B[0m\u001B[1;33m\u001B[0m\u001B[0m\n\u001B[0;32m 405\u001B[0m \u001B[1;33m\u001B[0m\u001B[0m\n\u001B[1;32m--> 406\u001B[1;33m raise AttributeError(\n\u001B[0m\u001B[0;32m 407\u001B[0m \"'{}' object and its delegates have no attribute '{}'\".format(\n\u001B[0;32m 408\u001B[0m self.__class__.__name__, key))\n", + "\u001B[1;31mAttributeError\u001B[0m: 'ProxyInstrumentModule' object and its delegates have no attribute 'test'" ] } ], From 71ddda50023b78974ee4d2a37a9bfdc8ea983053 Mon Sep 17 00:00:00 2001 From: marcosf2 Date: Tue, 21 Apr 2026 14:39:50 -0300 Subject: [PATCH 02/12] fix: test teardown and resource cleanup Terminate zmq contexts on disconnect, close instrument tabs in window closeEvents so SubClient threads exit, make refreshStationComponents defensive against None, and misc dummy-instrument/conftest fixes so test_client_station and most of test_server_gui pass cleanly. --- pyproject.toml | 10 + src/instrumentserver/blueprints.py | 4 +- src/instrumentserver/client/application.py | 7 + src/instrumentserver/client/core.py | 6 + src/instrumentserver/client/proxy.py | 9 + src/instrumentserver/gui/instruments.py | 16 + src/instrumentserver/log.py | 73 +- src/instrumentserver/monitoring/__init__.py | 0 src/instrumentserver/server/application.py | 19 +- .../testing/dummy_instruments/generic.py | 11 +- test/pytest/conftest.py | 32 +- test/pytest/test_base.py | 147 ++ test/pytest/test_client_station.py | 149 ++ test/pytest/test_config.py | 259 +++ test/pytest/test_helpers.py | 200 ++ test/pytest/test_server_gui.py | 79 +- uv.lock | 2036 +++++++++++++++++ 17 files changed, 2981 insertions(+), 76 deletions(-) create mode 100644 src/instrumentserver/monitoring/__init__.py create mode 100644 test/pytest/test_base.py create mode 100644 test/pytest/test_client_station.py create mode 100644 test/pytest/test_config.py create mode 100644 test/pytest/test_helpers.py create mode 100644 uv.lock diff --git a/pyproject.toml b/pyproject.toml index e907b9c..c8218d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,3 +36,13 @@ package-dir = {"" = "src"} [tool.setuptools.packages.find] where = ["src"] + +[tool.pytest.ini_options] +testpaths = ["test/pytest"] +qt_api = "pyqt5" + +[dependency-groups] +dev = [ + "pytest>=9.0.2", + "pytest-qt>=4.5.0", +] diff --git a/src/instrumentserver/blueprints.py b/src/instrumentserver/blueprints.py index e8524dd..8411841 100644 --- a/src/instrumentserver/blueprints.py +++ b/src/instrumentserver/blueprints.py @@ -771,7 +771,7 @@ def iterable_to_serialized_dict(iterable: Optional[Iterable[Any]] = None): converted_iterable.append(arg_dict) elif isinstance(item, complex): - arg_dict = dict(real=item.real, imag=item.imag, _class_type='complex') + arg_dict = dict(real=float(item.real), imag=float(item.imag), _class_type='complex') converted_iterable.append(arg_dict) else: @@ -806,7 +806,7 @@ def dict_to_serialized_dict(dct: Optional[Dict[str, Any]] = None): kwarg_dict = _convert_arbitrary_obj_to_dict(value) converted_dict[name] = kwarg_dict elif isinstance(value, complex): - kwarg_dict = dict(real=value.real, imag=value.imag, _class_type='complex') + kwarg_dict = dict(real=float(value.real), imag=float(value.imag), _class_type='complex') converted_dict[name] = kwarg_dict else: converted_dict[name] = str(value) diff --git a/src/instrumentserver/client/application.py b/src/instrumentserver/client/application.py index cd6b5cc..e4d1bdf 100644 --- a/src/instrumentserver/client/application.py +++ b/src/instrumentserver/client/application.py @@ -345,6 +345,13 @@ def removeInstrumentFromGui(self, name: str): def closeEvent(self, event: QtGui.QCloseEvent) -> None: """Cleanup listener thread before closing the window.""" + for name, widget in list(self.instrumentTabsOpen.items()): + try: + widget.close() + except Exception: + pass + self.instrumentTabsOpen.clear() + self.listener.stop() self.listenerThread.quit() self.listenerThread.wait(3000) # Wait up to 3 seconds for thread to finish diff --git a/src/instrumentserver/client/core.py b/src/instrumentserver/client/core.py index 27f65fb..a8dfa62 100644 --- a/src/instrumentserver/client/core.py +++ b/src/instrumentserver/client/core.py @@ -113,6 +113,12 @@ def disconnect(self): except Exception: pass self.socket = None + if self.context is not None: + try: + self.context.term() + except Exception: + pass + self.context = None self.connected = False diff --git a/src/instrumentserver/client/proxy.py b/src/instrumentserver/client/proxy.py index 513f53e..1de2862 100644 --- a/src/instrumentserver/client/proxy.py +++ b/src/instrumentserver/client/proxy.py @@ -799,6 +799,15 @@ def _create_instruments(self, instrument_dict: dict): def close_instrument(self, instrument_name:str): self.client.close_instrument(instrument_name) + def disconnect(self): + """Tear down the underlying client and release its zmq resources.""" + if self.client is not None: + try: + self.client.disconnect() + except Exception: + pass + self.client = None + @staticmethod def _remake_client_station_when_fail(func): """ diff --git a/src/instrumentserver/gui/instruments.py b/src/instrumentserver/gui/instruments.py index 3834e52..0cf28de 100644 --- a/src/instrumentserver/gui/instruments.py +++ b/src/instrumentserver/gui/instruments.py @@ -284,9 +284,18 @@ def __init__(self, *args, **kwargs): self.cliThread.started.connect(self.subClient.connect) self.subClient.update.connect(self.updateParameter) + self.subClient.finished.connect(self.cliThread.quit) self.cliThread.start() + def stopListener(self): + """Stop the background listener thread and wait for it to exit.""" + if self.subClient is not None: + self.subClient.stop() + if self.cliThread is not None: + self.cliThread.quit() + self.cliThread.wait(3000) + @QtCore.Slot(ParameterBroadcastBluePrint) def updateParameter(self, bp: ParameterBroadcastBluePrint): fullName = '.'.join(bp.name.split('.')[1:]) @@ -694,3 +703,10 @@ def __init__(self, ins: Union[ProxyInstrument, Instrument], parent=None, **model self.parametersList.view.resizeColumnToContents(0) self.parametersList.view.resizeColumnToContents(1) self.methodsList.view.resizeColumnToContents(0) + + def closeEvent(self, event: QtGui.QCloseEvent) -> None: + """Stop the parameter subscriber thread before destruction.""" + model = getattr(self.parametersList, 'model', None) + if model is not None and hasattr(model, 'stopListener'): + model.stopListener() + super().closeEvent(event) diff --git a/src/instrumentserver/log.py b/src/instrumentserver/log.py index cc5230e..edee369 100644 --- a/src/instrumentserver/log.py +++ b/src/instrumentserver/log.py @@ -60,39 +60,46 @@ def set_transform(self, fn): self._transform = fn def emit(self, record): - formatted = self.format(record) # prefix + message - raw_msg = record.getMessage() # message only - - # Color for prefix (log level) - clr = self.COLORS.get(record.levelno, QtGui.QColor('black')).name() - - if self._transform is not None: - html_fragment = self._transform(record, raw_msg) - if html_fragment: - i = formatted.rfind(raw_msg) - if i >= 0: - prefix = formatted[:i] - suffix = formatted[i + len(raw_msg):] - else: - prefix, suffix = "", "" - - # Build HTML line - html = ( - f"{escape(prefix)}" - f"{html_fragment}" - f"{escape(suffix)}" - ) - - # send to GUI thread - self.new_html.emit(html) - return - - # fallback: original plain text path - msg = formatted - clr_q = self.COLORS.get(record.levelno, QtGui.QColor('black')).name() - html = f"{escape(msg)}" - - self.new_html.emit(html) + try: + formatted = self.format(record) # prefix + message + raw_msg = record.getMessage() # message only + + # Color for prefix (log level) + clr = self.COLORS.get(record.levelno, QtGui.QColor('black')).name() + + if self._transform is not None: + html_fragment = self._transform(record, raw_msg) + if html_fragment: + i = formatted.rfind(raw_msg) + if i >= 0: + prefix = formatted[:i] + suffix = formatted[i + len(raw_msg):] + else: + prefix, suffix = "", "" + + # Build HTML line + html = ( + f"{escape(prefix)}" + f"{html_fragment}" + f"{escape(suffix)}" + ) + + # send to GUI thread + self.new_html.emit(html) + return + + # fallback: original plain text path + msg = formatted + clr_q = self.COLORS.get(record.levelno, QtGui.QColor('black')).name() + html = f"{escape(msg)}" + + self.new_html.emit(html) + except RuntimeError: + # Widget has been destroyed; detach self from the logger so we + # stop receiving further records and Python can collect us. + for lg in list(logging.Logger.manager.loggerDict.values()) + [logging.getLogger()]: + if isinstance(lg, logging.Logger) and self in lg.handlers: + lg.removeHandler(self) class LogWidget(QtWidgets.QWidget): """ diff --git a/src/instrumentserver/monitoring/__init__.py b/src/instrumentserver/monitoring/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/instrumentserver/server/application.py b/src/instrumentserver/server/application.py index cf3a8d2..2b2b4fa 100644 --- a/src/instrumentserver/server/application.py +++ b/src/instrumentserver/server/application.py @@ -590,9 +590,19 @@ def log(self, message, level=LogLevels.info): log(logger, message, level) def closeEvent(self, event): - if hasattr(self, 'stationServerThread'): + for name, widget in list(self.instrumentTabsOpen.items()): + try: + widget.close() + except Exception: + pass + self.instrumentTabsOpen.clear() + + if hasattr(self, 'stationServerThread') and self.stationServerThread is not None: if self.stationServerThread.isRunning(): - self.client.ask(self.stationServer.SAFEWORD) + try: + self.client.ask(self.stationServer.SAFEWORD) + except Exception: + pass event.accept() def startServer(self): @@ -673,7 +683,10 @@ def refreshStationComponents(self): """Clear and re-populate the widget holding the station components, using the objects that are currently registered in the station.""" self.stationList.clear() - for ins in self.client.list_instruments(): + instruments = self.client.list_instruments() + if not instruments: + return + for ins in instruments: bp = self.client.getBluePrint(ins) self.stationList.addInstrument(bp) self._bluePrints[ins] = bp diff --git a/src/instrumentserver/testing/dummy_instruments/generic.py b/src/instrumentserver/testing/dummy_instruments/generic.py index 1a64398..5f31353 100644 --- a/src/instrumentserver/testing/dummy_instruments/generic.py +++ b/src/instrumentserver/testing/dummy_instruments/generic.py @@ -23,8 +23,6 @@ def __init__(self, name: str, *args, **kwargs): vals=validators.Numbers(-1, 1), initial_value=1) - self.functions['dummy_function'] = self.dummy_function - def dummy_function(self, *args, **kwargs): """Dummy function for specific channels used for testing""" print(f'the dummy chanel: {self.name} has been activated with:') @@ -59,11 +57,14 @@ def __init__(self, name: str, address=None, first_arg=None, second_arg=None, *ar initial_value=1) for chan_name in ('A', 'B', 'C'): - channel = DummyChannel('Chan{}'.format(chan_name)) + channel = DummyChannel(f'{name}_Chan{chan_name}') self.add_submodule(chan_name, channel) - self.functions['test_func'] = self.test_func - self.functions['dummy_function'] = self.dummy_function + def ask_raw(self, cmd): + """Dummy ask_raw so *IDN? and similar SCPI queries don't explode the GUI.""" + if cmd.strip().upper().startswith('*IDN'): + return f'dummy,{self.name},0,0' + return '' def test_func(self, a, b, *args, c: List[int] = [10, 11], **kwargs): """ diff --git a/test/pytest/conftest.py b/test/pytest/conftest.py index 0bb6a84..5d2bb98 100644 --- a/test/pytest/conftest.py +++ b/test/pytest/conftest.py @@ -2,22 +2,46 @@ import pytest # type: ignore[import-not-found] from instrumentserver.server.core import startServer +from instrumentserver.client.core import BaseClient from instrumentserver.client.proxy import Client +@pytest.fixture(scope='session') +def qapp_session(): + """Ensure a QApplication exists for the entire test session. + + QThread (used by startServer) requires a running QApplication. + pytest-qt provides 'qapp' per-session, but only when qtbot is requested. + This fixture guarantees the app exists even for non-GUI tests. + """ + from instrumentserver import QtWidgets + app = QtWidgets.QApplication.instance() + if app is None: + app = QtWidgets.QApplication([]) + return app + + @pytest.fixture(scope='module') -def start_server(): +def start_server(qapp_session): server, thread = startServer() yield server - thread.quit() + # The zmq loop in StationServer blocks on poll(); thread.quit() on its own + # won't interrupt it. Send the SAFEWORD so the server shuts itself down, + # then wait for the thread's event loop to exit. + try: + with BaseClient() as shutdown_cli: + shutdown_cli.ask(server.SAFEWORD) + except Exception: + pass + thread.wait(5000) thread.deleteLater() - thread = None @pytest.fixture() def cli(start_server): cli = Client() - return cli + yield cli + cli.disconnect() @pytest.fixture() diff --git a/test/pytest/test_base.py b/test/pytest/test_base.py new file mode 100644 index 0000000..e98285c --- /dev/null +++ b/test/pytest/test_base.py @@ -0,0 +1,147 @@ +"""Unit tests for instrumentserver.base encode/decode and send/recv. + +Note: encode/decode in this module is designed to work with blueprint objects +that implement .toJson() — it is not a general-purpose JSON encoder. +""" +import time + +import pytest +import zmq + +from instrumentserver.base import encode, decode, send, recv, sendBroadcast, recvMultipart +from instrumentserver.blueprints import ( + ParameterBroadcastBluePrint, + ServerInstruction, + Operation, + bluePrintToDict, + deserialize_obj, +) + + +# --------------------------------------------------------------------------- +# encode / decode (blueprint objects only) +# --------------------------------------------------------------------------- + +def test_encode_broadcast_blueprint_returns_string(): + bp = ParameterBroadcastBluePrint(name='p', action='parameter-update', value=42, unit='V') + result = encode(bp) + assert isinstance(result, str) + + +def test_encode_decode_broadcast_blueprint_round_trip(): + bp = ParameterBroadcastBluePrint(name='p', action='parameter-update', value=42, unit='V') + encoded = encode(bp) + decoded = decode(encoded) + assert isinstance(decoded, ParameterBroadcastBluePrint) + assert decoded.name == 'p' + assert decoded.value == 42 + assert decoded.unit == 'V' + assert decoded.action == 'parameter-update' + + +def test_encode_decode_server_instruction_round_trip(): + instr = ServerInstruction(operation=Operation.get_existing_instruments) + encoded = encode(instr) + decoded = decode(encoded) + assert isinstance(decoded, ServerInstruction) + # operation is stored/restored as its string name + assert decoded.operation in ( + Operation.get_existing_instruments, + Operation.get_existing_instruments.name, + Operation.get_existing_instruments.value, + ) + + +def test_decode_string_is_returned_as_is(): + """encode wraps the string; decode should round-trip it.""" + s = '{"key": "value"}' + # encode on a plain string calls to_dict which returns it unchanged, + # then json.dumps wraps it as a JSON string + encoded = encode(s) + decoded = decode(encoded) + # decoded is the original string (json.loads unwraps, deserialize_obj tries + # to parse it as JSON again and returns the inner dict) + assert decoded == {'key': 'value'} + + +# --------------------------------------------------------------------------- +# send / recv (using zmq PAIR sockets with blueprint objects) +# --------------------------------------------------------------------------- + +@pytest.fixture +def zmq_pair(): + ctx = zmq.Context() + s1 = ctx.socket(zmq.PAIR) + s2 = ctx.socket(zmq.PAIR) + port = s1.bind_to_random_port("tcp://127.0.0.1") + s2.connect(f"tcp://127.0.0.1:{port}") + for s in (s1, s2): + s.setsockopt(zmq.RCVTIMEO, 2000) + s.setsockopt(zmq.LINGER, 0) + yield s1, s2 + s1.close() + s2.close() + ctx.term() + + +def test_send_recv_broadcast_blueprint(zmq_pair): + s1, s2 = zmq_pair + bp = ParameterBroadcastBluePrint(name='my_p', action='parameter-update', value=7, unit='Hz') + send(s1, bp) + result = recv(s2) + assert isinstance(result, ParameterBroadcastBluePrint) + assert result.name == 'my_p' + assert result.value == 7 + + +def test_send_recv_server_instruction(zmq_pair): + s1, s2 = zmq_pair + instr = ServerInstruction(operation=Operation.get_existing_instruments) + send(s1, instr) + result = recv(s2) + assert isinstance(result, ServerInstruction) + assert result.operation in ( + Operation.get_existing_instruments, + Operation.get_existing_instruments.name, + Operation.get_existing_instruments.value, + ) + + +# --------------------------------------------------------------------------- +# sendBroadcast / recvMultipart (PUB/SUB) +# --------------------------------------------------------------------------- + +@pytest.fixture +def zmq_pub_sub(): + ctx = zmq.Context() + pub = ctx.socket(zmq.PUB) + sub = ctx.socket(zmq.SUB) + port = pub.bind_to_random_port("tcp://127.0.0.1") + sub.connect(f"tcp://127.0.0.1:{port}") + sub.setsockopt_string(zmq.SUBSCRIBE, '') + sub.setsockopt(zmq.RCVTIMEO, 2000) + pub.setsockopt(zmq.LINGER, 0) + sub.setsockopt(zmq.LINGER, 0) + time.sleep(0.05) + yield pub, sub + pub.close() + sub.close() + ctx.term() + + +def test_sendBroadcast_recvMultipart(zmq_pub_sub): + pub, sub = zmq_pub_sub + bp = ParameterBroadcastBluePrint(name='my_param', action='parameter-update', value=7, unit='V') + sendBroadcast(pub, 'my_param', bp) + name, result = recvMultipart(sub) + assert name == 'my_param' + assert isinstance(result, ParameterBroadcastBluePrint) + assert result.value == 7 + + +def test_sendBroadcast_name_prefix_matches(zmq_pub_sub): + pub, sub = zmq_pub_sub + bp = ParameterBroadcastBluePrint(name='ins.param', action='parameter-set', value=42) + sendBroadcast(pub, 'ins.param', bp) + name, result = recvMultipart(sub) + assert name == 'ins.param' diff --git a/test/pytest/test_client_station.py b/test/pytest/test_client_station.py new file mode 100644 index 0000000..f9b9ecc --- /dev/null +++ b/test/pytest/test_client_station.py @@ -0,0 +1,149 @@ +"""Tests for ClientStation and ClientStationGui.""" +import json +from pathlib import Path + +import pytest + +from instrumentserver.client.proxy import ClientStation + +DUMMY_CLASS = 'instrumentserver.testing.dummy_instruments.generic.DummyInstrumentWithSubmodule' + + +# --------------------------------------------------------------------------- +# ClientStation (no GUI) +# --------------------------------------------------------------------------- + +@pytest.fixture(scope='module') +def client_station(start_server): + station = ClientStation(host='localhost', port=5555) + yield station + station.disconnect() + + +def test_client_station_creates_instruments(client_station): + ins = client_station.find_or_create_instrument('cs_dummy', DUMMY_CLASS) + assert 'cs_dummy' in client_station.instruments + + +def test_client_station_get_parameters(client_station): + client_station.find_or_create_instrument('cs_dummy', DUMMY_CLASS) + params = client_station.get_parameters() + assert isinstance(params, dict) + assert 'cs_dummy' in params + + +def test_client_station_set_parameters(client_station): + ins = client_station.find_or_create_instrument('cs_dummy', DUMMY_CLASS) + ins.param0(0) + client_station.set_parameters({'cs_dummy': {'param0': 1}}) + assert ins.param0() == 1 + + +def test_client_station_save_load_parameters(tmp_path, client_station): + ins = client_station.find_or_create_instrument('cs_dummy', DUMMY_CLASS) + ins.param0(1) + + file_path = str(tmp_path / 'params.json') + client_station.save_parameters(file_path) + + # Mutate the value + ins.param0(0) + assert ins.param0() == 0 + + # Load back + client_station.load_parameters(file_path) + assert ins.param0() == 1 + + +def test_client_station_get_instrument(client_station): + client_station.find_or_create_instrument('cs_dummy', DUMMY_CLASS) + retrieved = client_station.get_instrument('cs_dummy') + assert retrieved is not None + assert retrieved.name == 'cs_dummy' + + +def test_client_station_subscript_access(client_station): + client_station.find_or_create_instrument('cs_dummy', DUMMY_CLASS) + assert client_station['cs_dummy'] is client_station.instruments['cs_dummy'] + + +# --------------------------------------------------------------------------- +# ClientStationGui +# --------------------------------------------------------------------------- + +def test_client_station_gui_opens(qtbot, start_server): + from instrumentserver.client.application import ClientStationGui + station = ClientStation(host='localhost', port=5555) + window = ClientStationGui(station) + qtbot.addWidget(window) + try: + assert window is not None + finally: + window.close() + station.disconnect() + + +def test_client_station_gui_has_three_tabs(qtbot, start_server): + from instrumentserver.client.application import ClientStationGui + station = ClientStation(host='localhost', port=5555) + window = ClientStationGui(station) + qtbot.addWidget(window) + try: + tab_texts = [window.tabs.tabText(i) for i in range(window.tabs.count())] + assert 'Station' in tab_texts + assert 'Log' in tab_texts + assert 'Server' in tab_texts + finally: + window.close() + station.disconnect() + + +def test_client_station_gui_server_widget_shows_host_port(qtbot, start_server): + from instrumentserver.client.application import ClientStationGui + station = ClientStation(host='localhost', port=5555) + window = ClientStationGui(station) + qtbot.addWidget(window) + try: + assert window.server_widget.host.text() == 'localhost' + assert window.server_widget.port.text() == '5555' + finally: + window.close() + station.disconnect() + + +def test_client_station_gui_station_list_populated(qtbot, start_server): + from instrumentserver.client.application import ClientStationGui + from instrumentserver import QtCore + + station = ClientStation(host='localhost', port=5555) + station.find_or_create_instrument('gui_cs_dummy', DUMMY_CLASS) + window = ClientStationGui(station) + qtbot.addWidget(window) + try: + assert window.stationList.topLevelItemCount() >= 1 + finally: + window.close() + station.disconnect() + + +def test_client_station_gui_open_instrument_tab(qtbot, start_server): + from instrumentserver.client.application import ClientStationGui + from instrumentserver.gui.instruments import GenericInstrument + from instrumentserver import QtCore + + station = ClientStation(host='localhost', port=5555) + station.find_or_create_instrument('gui_cs_dummy2', DUMMY_CLASS) + window = ClientStationGui(station) + qtbot.addWidget(window) + try: + items = window.stationList.findItems( + 'gui_cs_dummy2', QtCore.Qt.MatchExactly | QtCore.Qt.MatchRecursive, 0 + ) + assert len(items) > 0 + + window.openInstrumentTab(items[0], 0) + assert 'gui_cs_dummy2' in window.instrumentTabsOpen + assert isinstance(window.instrumentTabsOpen['gui_cs_dummy2'], GenericInstrument) + finally: + window.close() + station.disconnect() diff --git a/test/pytest/test_config.py b/test/pytest/test_config.py new file mode 100644 index 0000000..09e1d45 --- /dev/null +++ b/test/pytest/test_config.py @@ -0,0 +1,259 @@ +"""Tests for instrumentserver.config.loadConfig.""" +import pytest +from pathlib import Path + +from instrumentserver.config import loadConfig, GUIFIELD + + +def _write_config(tmp_path: Path, content: str) -> Path: + p = tmp_path / "config.yml" + p.write_text(content) + return p + + +# --------------------------------------------------------------------------- +# Basic parsing +# --------------------------------------------------------------------------- + +def test_minimal_config(tmp_path): + cfg = _write_config(tmp_path, """\ +instruments: + my_ins: + type: instrumentserver.testing.dummy_instruments.generic.DummyInstrumentWithSubmodule +""") + path, serverConfig, fullConfig, tempFile, pollingRates, ipAddresses = loadConfig(cfg) + tempFile.close() + + assert 'my_ins' in serverConfig + assert 'my_ins' in fullConfig + assert pollingRates == {} + assert ipAddresses == {} + # returned path is a string + assert isinstance(path, str) + + +def test_temp_file_is_readable(tmp_path): + cfg = _write_config(tmp_path, """\ +instruments: + my_ins: + type: some.Type +""") + tempFilePath, _, _, tempFile, _, _ = loadConfig(cfg) + tempFile.seek(0) + content = tempFile.read() + assert len(content) > 0 + tempFile.close() + + +# --------------------------------------------------------------------------- +# SERVERFIELDS defaults +# --------------------------------------------------------------------------- + +def test_initialize_defaults_to_true(tmp_path): + cfg = _write_config(tmp_path, """\ +instruments: + my_ins: + type: some.Type +""") + _, serverConfig, _, tempFile, _, _ = loadConfig(cfg) + tempFile.close() + assert serverConfig['my_ins']['initialize'] is True + + +def test_initialize_explicit_false(tmp_path): + cfg = _write_config(tmp_path, """\ +instruments: + my_ins: + type: some.Type + initialize: false +""") + _, serverConfig, _, tempFile, _, _ = loadConfig(cfg) + tempFile.close() + assert serverConfig['my_ins']['initialize'] is False + + +def test_initialize_null_raises(tmp_path): + cfg = _write_config(tmp_path, """\ +instruments: + my_ins: + type: some.Type + initialize: +""") + with pytest.raises(AttributeError): + loadConfig(cfg) + + +# --------------------------------------------------------------------------- +# GUI field defaults +# --------------------------------------------------------------------------- + +def test_gui_defaults_to_generic_instrument(tmp_path): + cfg = _write_config(tmp_path, """\ +instruments: + my_ins: + type: some.Type +""") + _, _, fullConfig, tempFile, _, _ = loadConfig(cfg) + tempFile.close() + assert fullConfig['my_ins']['gui']['type'] == GUIFIELD['type'] + + +def test_gui_generic_alias_maps_to_full_path(tmp_path): + cfg = _write_config(tmp_path, """\ +instruments: + my_ins: + type: some.Type + gui: + type: generic +""") + _, _, fullConfig, tempFile, _, _ = loadConfig(cfg) + tempFile.close() + assert fullConfig['my_ins']['gui']['type'] == GUIFIELD['type'] + + +def test_gui_null_raises(tmp_path): + cfg = _write_config(tmp_path, """\ +instruments: + my_ins: + type: some.Type + gui: +""") + with pytest.raises(AttributeError): + loadConfig(cfg) + + +# --------------------------------------------------------------------------- +# Error: missing instruments key +# --------------------------------------------------------------------------- + +def test_missing_instruments_key_raises(tmp_path): + cfg = _write_config(tmp_path, """\ +my_ins: + type: some.Type +""") + with pytest.raises(AttributeError): + loadConfig(cfg) + + +# --------------------------------------------------------------------------- +# pollingRate +# --------------------------------------------------------------------------- + +def test_polling_rate_parsed(tmp_path): + cfg = _write_config(tmp_path, """\ +instruments: + my_ins: + type: some.Type + pollingRate: + param1: 100 + param2: 200 +""") + _, _, _, tempFile, pollingRates, _ = loadConfig(cfg) + tempFile.close() + assert pollingRates == {'my_ins.param1': 100, 'my_ins.param2': 200} + + +def test_polling_rate_empty_is_ignored(tmp_path): + cfg = _write_config(tmp_path, """\ +instruments: + my_ins: + type: some.Type + pollingRate: +""") + _, _, _, tempFile, pollingRates, _ = loadConfig(cfg) + tempFile.close() + assert pollingRates == {} + + +# --------------------------------------------------------------------------- +# networking +# --------------------------------------------------------------------------- + +def test_networking_parsed(tmp_path): + cfg = _write_config(tmp_path, """\ +instruments: + my_ins: + type: some.Type +networking: + externalBroadcast: tcp://192.168.1.1:5556 + listeningAddress: 192.168.1.1 +""") + _, _, _, tempFile, _, ipAddresses = loadConfig(cfg) + tempFile.close() + assert ipAddresses['externalBroadcast'] == 'tcp://192.168.1.1:5556' + assert ipAddresses['listeningAddress'] == '192.168.1.1' + + +def test_no_networking_section_gives_empty_dict(tmp_path): + cfg = _write_config(tmp_path, """\ +instruments: + my_ins: + type: some.Type +""") + _, _, _, tempFile, _, ipAddresses = loadConfig(cfg) + tempFile.close() + assert ipAddresses == {} + + +# --------------------------------------------------------------------------- +# gui_defaults merging +# --------------------------------------------------------------------------- + +def test_gui_defaults_default_section(tmp_path): + cfg = _write_config(tmp_path, """\ +instruments: + my_ins: + type: some.module.MyClass +gui_defaults: + __default__: + parameters-hide: + - IDN +""") + _, _, fullConfig, tempFile, _, _ = loadConfig(cfg) + tempFile.close() + kwargs = fullConfig['my_ins']['gui'].get('kwargs', {}) + assert 'parameters-hide' in kwargs + assert 'IDN' in kwargs['parameters-hide'] + + +def test_gui_defaults_class_section(tmp_path): + cfg = _write_config(tmp_path, """\ +instruments: + my_ins: + type: some.module.MyClass +gui_defaults: + MyClass: + parameters-hide: + - power_level +""") + _, _, fullConfig, tempFile, _, _ = loadConfig(cfg) + tempFile.close() + kwargs = fullConfig['my_ins']['gui'].get('kwargs', {}) + assert 'parameters-hide' in kwargs + assert 'power_level' in kwargs['parameters-hide'] + + +def test_gui_defaults_merging_order(tmp_path): + """__default__ + class + instance patterns all appear in merged result.""" + cfg = _write_config(tmp_path, """\ +instruments: + my_ins: + type: some.module.MyClass + gui: + kwargs: + parameters-hide: + - instance_param +gui_defaults: + __default__: + parameters-hide: + - default_param + MyClass: + parameters-hide: + - class_param +""") + _, _, fullConfig, tempFile, _, _ = loadConfig(cfg) + tempFile.close() + hide = fullConfig['my_ins']['gui']['kwargs']['parameters-hide'] + assert 'default_param' in hide + assert 'class_param' in hide + assert 'instance_param' in hide \ No newline at end of file diff --git a/test/pytest/test_helpers.py b/test/pytest/test_helpers.py new file mode 100644 index 0000000..77d92fd --- /dev/null +++ b/test/pytest/test_helpers.py @@ -0,0 +1,200 @@ +import pytest + +from instrumentserver.helpers import ( + stringToArgsAndKwargs, + flat_to_nested_dict, + flatten_dict, + is_flat_dict, + nestedAttributeFromString, + typeClassPath, + objectClassPath, +) + + +# --------------------------------------------------------------------------- +# stringToArgsAndKwargs +# --------------------------------------------------------------------------- + +def test_stringToArgsAndKwargs_empty_string(): + args, kwargs = stringToArgsAndKwargs("") + assert args == [] + assert kwargs == {} + + +def test_stringToArgsAndKwargs_whitespace_only(): + args, kwargs = stringToArgsAndKwargs(" ") + assert args == [] + assert kwargs == {} + + +@pytest.mark.parametrize("value, expected_args, expected_kwargs", [ + ("1, True", [1, True], {}), + ("'hello'", ['hello'], {}), + ("1, 2, 3", [1, 2, 3], {}), + ("x=1, y=2", [], {'x': 1, 'y': 2}), + ("1, abc=12.3", [1], {'abc': 12.3}), +]) +def test_stringToArgsAndKwargs_valid(value, expected_args, expected_kwargs): + args, kwargs = stringToArgsAndKwargs(value) + assert args == expected_args + assert kwargs == expected_kwargs + + +def test_stringToArgsAndKwargs_bad_kwarg_format(): + with pytest.raises(ValueError): + stringToArgsAndKwargs("a=1=2") + + +def test_stringToArgsAndKwargs_unevaluable_arg(): + with pytest.raises(ValueError): + stringToArgsAndKwargs("undefined_variable_xyz_abc") + + +def test_stringToArgsAndKwargs_unevaluable_kwarg_value(): + with pytest.raises(ValueError): + stringToArgsAndKwargs("x=undefined_variable_xyz_abc") + + +# --------------------------------------------------------------------------- +# flat_to_nested_dict +# --------------------------------------------------------------------------- + +def test_flat_to_nested_dict_already_flat(): + flat = {"a": 1, "b": 2} + result = flat_to_nested_dict(flat) + assert result == {"a": 1, "b": 2} + + +def test_flat_to_nested_dict_single_level(): + flat = {"a.b": 1, "a.c": 2} + result = flat_to_nested_dict(flat) + assert result == {"a": {"b": 1, "c": 2}} + + +def test_flat_to_nested_dict_multi_level(): + flat = {"a.b.c": 1, "a.b.d": 2, "x": 3} + result = flat_to_nested_dict(flat) + assert result == {"a": {"b": {"c": 1, "d": 2}}, "x": 3} + + +def test_flat_to_nested_dict_empty(): + assert flat_to_nested_dict({}) == {} + + +# --------------------------------------------------------------------------- +# flatten_dict +# --------------------------------------------------------------------------- + +def test_flatten_dict_already_flat(): + d = {"a": 1, "b": 2} + result = flatten_dict(d) + assert result == {"a": 1, "b": 2} + + +def test_flatten_dict_nested(): + nested = {"a": {"b": 1}, "x": 3} + result = flatten_dict(nested) + assert result == {"a.b": 1, "x": 3} + + +def test_flatten_dict_custom_sep(): + nested = {"a": {"b": 1}} + result = flatten_dict(nested, sep='/') + assert result == {"a/b": 1} + + +def test_flatten_dict_round_trip(): + flat = {"a.b.c": 1, "a.b.d": 2, "x": 3} + nested = flat_to_nested_dict(flat) + back_to_flat = flatten_dict(nested) + assert back_to_flat == flat + + +# --------------------------------------------------------------------------- +# is_flat_dict +# --------------------------------------------------------------------------- + +def test_is_flat_dict_flat(): + assert is_flat_dict({"a": 1, "b": "hello"}) is True + + +def test_is_flat_dict_nested(): + assert is_flat_dict({"a": {"b": 1}}) is False + + +def test_is_flat_dict_mixed(): + assert is_flat_dict({"a": 1, "b": {"c": 2}}) is False + + +def test_is_flat_dict_empty(): + assert is_flat_dict({}) is True + + +# --------------------------------------------------------------------------- +# nestedAttributeFromString +# --------------------------------------------------------------------------- + +class _Root: + class _Child: + value = 42 + + scalar = 99 + + +def test_nestedAttributeFromString_single_level(): + root = _Root() + assert nestedAttributeFromString(root, 'scalar') == 99 + + +def test_nestedAttributeFromString_two_levels(): + root = _Root() + assert nestedAttributeFromString(root, '_Child.value') == 42 + + +def test_nestedAttributeFromString_missing_raises(): + root = _Root() + with pytest.raises(AttributeError): + nestedAttributeFromString(root, 'nonexistent_attr') + + +def test_nestedAttributeFromString_nested_missing_raises(): + root = _Root() + with pytest.raises(AttributeError): + nestedAttributeFromString(root, '_Child.nonexistent') + + +# --------------------------------------------------------------------------- +# typeClassPath / objectClassPath +# --------------------------------------------------------------------------- + +class _MyClass: + pass + + +def test_typeClassPath_contains_class_name(): + path = typeClassPath(_MyClass) + assert '_MyClass' in path + assert '.' in path + + +def test_objectClassPath_contains_class_name(): + obj = _MyClass() + path = objectClassPath(obj) + assert '_MyClass' in path + assert '.' in path + + +def test_typeClassPath_builtin(): + path = typeClassPath(int) + assert 'int' in path + + +def test_objectClassPath_builtin_instance(): + path = objectClassPath(42) + assert 'int' in path + + +def test_typeClassPath_and_objectClassPath_agree(): + """typeClassPath on the class and objectClassPath on an instance should match.""" + obj = _MyClass() + assert typeClassPath(_MyClass) == objectClassPath(obj) \ No newline at end of file diff --git a/test/pytest/test_server_gui.py b/test/pytest/test_server_gui.py index 849c05f..106c847 100644 --- a/test/pytest/test_server_gui.py +++ b/test/pytest/test_server_gui.py @@ -3,9 +3,25 @@ from instrumentserver import QtCore from instrumentserver.gui.instruments import GenericInstrument +from instrumentserver.helpers import flatten_dict from instrumentserver.server.application import startServerGuiApplication +def _shutdown_server_window(window): + """Trigger closeEvent and wait for the server thread to exit so the next + test can bind the same port again.""" + try: + window.close() + except Exception: + pass + thread = getattr(window, 'stationServerThread', None) + if thread is not None: + try: + thread.wait(5000) + except Exception: + pass + + def test_saving_button(qtbot): correct_file_dict = { "rr.bandwidth": 10000.0, @@ -35,10 +51,11 @@ def test_saving_button(qtbot): assert file_path.is_file() with open(str(file_path), 'r') as f: loaded_file = json.load(f) - assert correct_file_dict == loaded_file + assert correct_file_dict == flatten_dict(loaded_file) finally: file_path.unlink(missing_ok=True) + _shutdown_server_window(window) def test_loading_button(qtbot): @@ -72,21 +89,24 @@ def test_loading_button(qtbot): finally: file_path.unlink(missing_ok=True) + _shutdown_server_window(window) def test_refresh_button(qtbot): window = startServerGuiApplication() qtbot.addWidget(window) + try: + assert window.stationList.topLevelItemCount() == 0 - assert window.stationList.topLevelItemCount() == 0 + dummy = window.client.find_or_create_instrument('dummy', + 'instrumentserver.testing.dummy_instruments.generic.DummyInstrumentWithSubmodule') - dummy = window.client.find_or_create_instrument('dummy', - 'instrumentserver.testing.dummy_instruments.generic.DummyInstrumentWithSubmodule') + refresh_widget = window.toolBar.widgetForAction(window.refreshStationAction) + qtbot.mouseClick(refresh_widget, QtCore.Qt.LeftButton) - refresh_widget = window.toolBar.widgetForAction(window.refreshStationAction) - qtbot.mouseClick(refresh_widget, QtCore.Qt.LeftButton) - - assert window.stationList.topLevelItemCount() == 1 + assert window.stationList.topLevelItemCount() == 1 + finally: + _shutdown_server_window(window) def test_clicking_an_item(qtbot): @@ -94,36 +114,37 @@ def test_clicking_an_item(qtbot): window = startServerGuiApplication() qtbot.addWidget(window) + try: + assert window.stationList.topLevelItemCount() == 0 - assert window.stationList.topLevelItemCount() == 0 + dummy = window.client.find_or_create_instrument('dummy', + 'instrumentserver.testing.dummy_instruments.generic.DummyInstrumentWithSubmodule') - dummy = window.client.find_or_create_instrument('dummy', - 'instrumentserver.testing.dummy_instruments.generic.DummyInstrumentWithSubmodule') + window.refreshStationAction.trigger() + item = window.stationList.findItems('dummy', QtCore.Qt.MatchExactly | QtCore.Qt.MatchRecursive, 0) + widget = item[0].treeWidget() + qtbot.mouseClick(widget, QtCore.Qt.LeftButton) - window.refreshStationAction.trigger() - item = window.stationList.findItems('dummy', QtCore.Qt.MatchExactly | QtCore.Qt.MatchRecursive, 0) - widget = item[0].treeWidget() - qtbot.mouseClick(widget, QtCore.Qt.LeftButton) - - assert True + assert True + finally: + _shutdown_server_window(window) def test_opening_new_tab_generic_object(qtbot): window = startServerGuiApplication() qtbot.addWidget(window) + try: + dummy = window.client.find_or_create_instrument('dummy', + 'instrumentserver.testing.dummy_instruments.generic.DummyInstrumentWithSubmodule') - dummy = window.client.find_or_create_instrument('dummy', - 'instrumentserver.testing.dummy_instruments.generic.DummyInstrumentWithSubmodule') - - window.refreshStationAction.trigger() - item = window.stationList.findItems('dummy', QtCore.Qt.MatchExactly | QtCore.Qt.MatchRecursive, 0) - - # Manually triggering the tab opening since qtbot refuses to double-click an item - window.addInstrumentTab(item[0], 0) - - assert 'dummy' in window.instrumentTabsOpen - - assert isinstance(window.instrumentTabsOpen['dummy'], GenericInstrument) + window.refreshStationAction.trigger() + item = window.stationList.findItems('dummy', QtCore.Qt.MatchExactly | QtCore.Qt.MatchRecursive, 0) + # Manually triggering the tab opening since qtbot refuses to double-click an item + window.addInstrumentTab(item[0], 0) + assert 'dummy' in window.instrumentTabsOpen + assert isinstance(window.instrumentTabsOpen['dummy'], GenericInstrument) + finally: + _shutdown_server_window(window) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..9dc9a10 --- /dev/null +++ b/uv.lock @@ -0,0 +1,2036 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version < '3.12' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] + +[[package]] +name = "appnope" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, +] + +[[package]] +name = "asttokens" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "broadbean" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib" }, + { name = "numpy" }, + { name = "schema" }, + { name = "versioningit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/c9/c64ec69941544561f503fe092f2f32b9117252b2dd22b90368787d2316a2/broadbean-0.14.0.tar.gz", hash = "sha256:bfe3afea69529da246f7ca2803d0213c625f96b15a7ca4283b9c22f8fc5c655c", size = 44803, upload-time = "2024-03-06T22:15:44.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/4d/2b3b4b35456176182d45cdf977fdea80bf71be563f4074536ec3436eed9c/broadbean-0.14.0-py3-none-any.whl", hash = "sha256:7a9195ef16241853e2ea20aedc6f67ee72f5464a463b3584fcbedcb63daf88e7", size = 36755, upload-time = "2024-03-06T22:15:42.41Z" }, +] + +[[package]] +name = "cf-xarray" +version = "0.10.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "xarray" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/78/f4f38e7ea6221773ea48d85c00d529b1fdc7378a1a1b77c2b77661446a0b/cf_xarray-0.10.11.tar.gz", hash = "sha256:e10ee37b0ed3ba36f42346360f2bc070c690ddc73bb9dcdd9463b3a221453be3", size = 686693, upload-time = "2026-02-03T19:17:42.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/ce/5c4f4660da5521d90bea62cdf8396d7e4ce4a00513e218d267b97f9ea453/cf_xarray-0.10.11-py3-none-any.whl", hash = "sha256:c47fff625766c69a66fedef368d9787acb0819b32d8bd022f8b045089b42109a", size = 78421, upload-time = "2026-02-03T19:17:40.431Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "comm" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "dask" +version = "2026.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "cloudpickle" }, + { name = "fsspec" }, + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "packaging" }, + { name = "partd" }, + { name = "pyyaml" }, + { name = "toolz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/52/b0f9172b22778def907db1ff173249e4eb41f054b46a9c83b1528aaf811f/dask-2026.1.2.tar.gz", hash = "sha256:1136683de2750d98ea792670f7434e6c1cfce90cab2cc2f2495a9e60fd25a4fc", size = 10997838, upload-time = "2026-01-30T21:04:20.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/23/d39ccc4ed76222db31530b0a7d38876fdb7673e23f838e8d8f0ed4651a4f/dask-2026.1.2-py3-none-any.whl", hash = "sha256:46a0cf3b8d87f78a3d2e6b145aea4418a6d6d606fe6a16c79bd8ca2bb862bc91", size = 1482084, upload-time = "2026-01-30T21:04:18.363Z" }, +] + +[[package]] +name = "debugpy" +version = "1.8.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/cd8080344452e4874aae67c40d8940e2b4d47b01601a8fd9f44786c757c7/debugpy-1.8.20.tar.gz", hash = "sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33", size = 1645207, upload-time = "2026-01-29T23:03:28.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/56/c3baf5cbe4dd77427fd9aef99fcdade259ad128feeb8a786c246adb838e5/debugpy-1.8.20-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:eada6042ad88fa1571b74bd5402ee8b86eded7a8f7b827849761700aff171f1b", size = 2208318, upload-time = "2026-01-29T23:03:36.481Z" }, + { url = "https://files.pythonhosted.org/packages/9a/7d/4fa79a57a8e69fe0d9763e98d1110320f9ecd7f1f362572e3aafd7417c9d/debugpy-1.8.20-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:7de0b7dfeedc504421032afba845ae2a7bcc32ddfb07dae2c3ca5442f821c344", size = 3171493, upload-time = "2026-01-29T23:03:37.775Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f2/1e8f8affe51e12a26f3a8a8a4277d6e60aa89d0a66512f63b1e799d424a4/debugpy-1.8.20-cp311-cp311-win32.whl", hash = "sha256:773e839380cf459caf73cc533ea45ec2737a5cc184cf1b3b796cd4fd98504fec", size = 5209240, upload-time = "2026-01-29T23:03:39.109Z" }, + { url = "https://files.pythonhosted.org/packages/d5/92/1cb532e88560cbee973396254b21bece8c5d7c2ece958a67afa08c9f10dc/debugpy-1.8.20-cp311-cp311-win_amd64.whl", hash = "sha256:1f7650546e0eded1902d0f6af28f787fa1f1dbdbc97ddabaf1cd963a405930cb", size = 5233481, upload-time = "2026-01-29T23:03:40.659Z" }, + { url = "https://files.pythonhosted.org/packages/14/57/7f34f4736bfb6e00f2e4c96351b07805d83c9a7b33d28580ae01374430f7/debugpy-1.8.20-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ae3135e2089905a916909ef31922b2d733d756f66d87345b3e5e52b7a55f13d", size = 2550686, upload-time = "2026-01-29T23:03:42.023Z" }, + { url = "https://files.pythonhosted.org/packages/ab/78/b193a3975ca34458f6f0e24aaf5c3e3da72f5401f6054c0dfd004b41726f/debugpy-1.8.20-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:88f47850a4284b88bd2bfee1f26132147d5d504e4e86c22485dfa44b97e19b4b", size = 4310588, upload-time = "2026-01-29T23:03:43.314Z" }, + { url = "https://files.pythonhosted.org/packages/c1/55/f14deb95eaf4f30f07ef4b90a8590fc05d9e04df85ee379712f6fb6736d7/debugpy-1.8.20-cp312-cp312-win32.whl", hash = "sha256:4057ac68f892064e5f98209ab582abfee3b543fb55d2e87610ddc133a954d390", size = 5331372, upload-time = "2026-01-29T23:03:45.526Z" }, + { url = "https://files.pythonhosted.org/packages/a1/39/2bef246368bd42f9bd7cba99844542b74b84dacbdbea0833e610f384fee8/debugpy-1.8.20-cp312-cp312-win_amd64.whl", hash = "sha256:a1a8f851e7cf171330679ef6997e9c579ef6dd33c9098458bd9986a0f4ca52e3", size = 5372835, upload-time = "2026-01-29T23:03:47.245Z" }, + { url = "https://files.pythonhosted.org/packages/15/e2/fc500524cc6f104a9d049abc85a0a8b3f0d14c0a39b9c140511c61e5b40b/debugpy-1.8.20-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:5dff4bb27027821fdfcc9e8f87309a28988231165147c31730128b1c983e282a", size = 2539560, upload-time = "2026-01-29T23:03:48.738Z" }, + { url = "https://files.pythonhosted.org/packages/90/83/fb33dcea789ed6018f8da20c5a9bc9d82adc65c0c990faed43f7c955da46/debugpy-1.8.20-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:84562982dd7cf5ebebfdea667ca20a064e096099997b175fe204e86817f64eaf", size = 4293272, upload-time = "2026-01-29T23:03:50.169Z" }, + { url = "https://files.pythonhosted.org/packages/a6/25/b1e4a01bfb824d79a6af24b99ef291e24189080c93576dfd9b1a2815cd0f/debugpy-1.8.20-cp313-cp313-win32.whl", hash = "sha256:da11dea6447b2cadbf8ce2bec59ecea87cc18d2c574980f643f2d2dfe4862393", size = 5331208, upload-time = "2026-01-29T23:03:51.547Z" }, + { url = "https://files.pythonhosted.org/packages/13/f7/a0b368ce54ffff9e9028c098bd2d28cfc5b54f9f6c186929083d4c60ba58/debugpy-1.8.20-cp313-cp313-win_amd64.whl", hash = "sha256:eb506e45943cab2efb7c6eafdd65b842f3ae779f020c82221f55aca9de135ed7", size = 5372930, upload-time = "2026-01-29T23:03:53.585Z" }, + { url = "https://files.pythonhosted.org/packages/33/2e/f6cb9a8a13f5058f0a20fe09711a7b726232cd5a78c6a7c05b2ec726cff9/debugpy-1.8.20-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9c74df62fc064cd5e5eaca1353a3ef5a5d50da5eb8058fcef63106f7bebe6173", size = 2538066, upload-time = "2026-01-29T23:03:54.999Z" }, + { url = "https://files.pythonhosted.org/packages/c5/56/6ddca50b53624e1ca3ce1d1e49ff22db46c47ea5fb4c0cc5c9b90a616364/debugpy-1.8.20-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:077a7447589ee9bc1ff0cdf443566d0ecf540ac8aa7333b775ebcb8ce9f4ecad", size = 4269425, upload-time = "2026-01-29T23:03:56.518Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d9/d64199c14a0d4c476df46c82470a3ce45c8d183a6796cfb5e66533b3663c/debugpy-1.8.20-cp314-cp314-win32.whl", hash = "sha256:352036a99dd35053b37b7803f748efc456076f929c6a895556932eaf2d23b07f", size = 5331407, upload-time = "2026-01-29T23:03:58.481Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d9/1f07395b54413432624d61524dfd98c1a7c7827d2abfdb8829ac92638205/debugpy-1.8.20-cp314-cp314-win_amd64.whl", hash = "sha256:a98eec61135465b062846112e5ecf2eebb855305acc1dfbae43b72903b8ab5be", size = 5372521, upload-time = "2026-01-29T23:03:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658, upload-time = "2026-01-29T23:04:17.404Z" }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, +] + +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + +[[package]] +name = "fonttools" +version = "4.62.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/08/7012b00a9a5874311b639c3920270c36ee0c445b69d9989a85e5c92ebcb0/fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d", size = 3580737, upload-time = "2026-03-13T13:54:25.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/23ff32561ec8d45a4d48578b4d241369d9270dc50926c017570e60893701/fonttools-4.62.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:40975849bac44fb0b9253d77420c6d8b523ac4dcdcefeff6e4d706838a5b80f7", size = 2871039, upload-time = "2026-03-13T13:52:33.127Z" }, + { url = "https://files.pythonhosted.org/packages/24/7f/66d3f8a9338a9b67fe6e1739f47e1cd5cee78bd3bc1206ef9b0b982289a5/fonttools-4.62.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9dde91633f77fa576879a0c76b1d89de373cae751a98ddf0109d54e173b40f14", size = 2416346, upload-time = "2026-03-13T13:52:35.676Z" }, + { url = "https://files.pythonhosted.org/packages/aa/53/5276ceba7bff95da7793a07c5284e1da901cf00341ce5e2f3273056c0cca/fonttools-4.62.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6acb4109f8bee00fec985c8c7afb02299e35e9c94b57287f3ea542f28bd0b0a7", size = 5100897, upload-time = "2026-03-13T13:52:38.102Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a1/40a5c4d8e28b0851d53a8eeeb46fbd73c325a2a9a165f290a5ed90e6c597/fonttools-4.62.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1c5c25671ce8805e0d080e2ffdeca7f1e86778c5cbfbeae86d7f866d8830517b", size = 5071078, upload-time = "2026-03-13T13:52:41.305Z" }, + { url = "https://files.pythonhosted.org/packages/e3/be/d378fca4c65ea1956fee6d90ace6e861776809cbbc5af22388a090c3c092/fonttools-4.62.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a5d8825e1140f04e6c99bb7d37a9e31c172f3bc208afbe02175339e699c710e1", size = 5076908, upload-time = "2026-03-13T13:52:44.122Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d9/ae6a1d0693a4185a84605679c8a1f719a55df87b9c6e8e817bfdd9ef5936/fonttools-4.62.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:268abb1cb221e66c014acc234e872b7870d8b5d4657a83a8f4205094c32d2416", size = 5202275, upload-time = "2026-03-13T13:52:46.591Z" }, + { url = "https://files.pythonhosted.org/packages/54/6c/af95d9c4efb15cabff22642b608342f2bd67137eea6107202d91b5b03184/fonttools-4.62.1-cp311-cp311-win32.whl", hash = "sha256:942b03094d7edbb99bdf1ae7e9090898cad7bf9030b3d21f33d7072dbcb51a53", size = 2293075, upload-time = "2026-03-13T13:52:48.711Z" }, + { url = "https://files.pythonhosted.org/packages/d3/97/bf54c5b3f2be34e1f143e6db838dfdc54f2ffa3e68c738934c82f3b2a08d/fonttools-4.62.1-cp311-cp311-win_amd64.whl", hash = "sha256:e8514f4924375f77084e81467e63238b095abda5107620f49421c368a6017ed2", size = 2344593, upload-time = "2026-03-13T13:52:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/47/d4/dbacced3953544b9a93088cc10ef2b596d348c983d5c67a404fa41ec51ba/fonttools-4.62.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974", size = 2870219, upload-time = "2026-03-13T13:52:53.664Z" }, + { url = "https://files.pythonhosted.org/packages/66/9e/a769c8e99b81e5a87ab7e5e7236684de4e96246aae17274e5347d11ebd78/fonttools-4.62.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12859ff0b47dd20f110804c3e0d0970f7b832f561630cd879969011541a464a9", size = 2414891, upload-time = "2026-03-13T13:52:56.493Z" }, + { url = "https://files.pythonhosted.org/packages/69/64/f19a9e3911968c37e1e620e14dfc5778299e1474f72f4e57c5ec771d9489/fonttools-4.62.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c125ffa00c3d9003cdaaf7f2c79e6e535628093e14b5de1dccb08859b680936", size = 5033197, upload-time = "2026-03-13T13:52:59.179Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8a/99c8b3c3888c5c474c08dbfd7c8899786de9604b727fcefb055b42c84bba/fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392", size = 4988768, upload-time = "2026-03-13T13:53:02.761Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c6/0f904540d3e6ab463c1243a0d803504826a11604c72dd58c2949796a1762/fonttools-4.62.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04", size = 4971512, upload-time = "2026-03-13T13:53:05.678Z" }, + { url = "https://files.pythonhosted.org/packages/29/0b/5cbef6588dc9bd6b5c9ad6a4d5a8ca384d0cea089da31711bbeb4f9654a6/fonttools-4.62.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:19177c8d96c7c36359266e571c5173bcee9157b59cfc8cb0153c5673dc5a3a7d", size = 5122723, upload-time = "2026-03-13T13:53:08.662Z" }, + { url = "https://files.pythonhosted.org/packages/4a/47/b3a5342d381595ef439adec67848bed561ab7fdb1019fa522e82101b7d9c/fonttools-4.62.1-cp312-cp312-win32.whl", hash = "sha256:a24decd24d60744ee8b4679d38e88b8303d86772053afc29b19d23bb8207803c", size = 2281278, upload-time = "2026-03-13T13:53:10.998Z" }, + { url = "https://files.pythonhosted.org/packages/28/b1/0c2ab56a16f409c6c8a68816e6af707827ad5d629634691ff60a52879792/fonttools-4.62.1-cp312-cp312-win_amd64.whl", hash = "sha256:9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42", size = 2331414, upload-time = "2026-03-13T13:53:13.992Z" }, + { url = "https://files.pythonhosted.org/packages/3b/56/6f389de21c49555553d6a5aeed5ac9767631497ac836c4f076273d15bd72/fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79", size = 2865155, upload-time = "2026-03-13T13:53:16.132Z" }, + { url = "https://files.pythonhosted.org/packages/03/c5/0e3966edd5ec668d41dfe418787726752bc07e2f5fd8c8f208615e61fa89/fonttools-4.62.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe", size = 2412802, upload-time = "2026-03-13T13:53:18.878Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/e6ac4b44026de7786fe46e3bfa0c87e51d5d70a841054065d49cd62bb909/fonttools-4.62.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68", size = 5013926, upload-time = "2026-03-13T13:53:21.379Z" }, + { url = "https://files.pythonhosted.org/packages/e2/98/8b1e801939839d405f1f122e7d175cebe9aeb4e114f95bfc45e3152af9a7/fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1", size = 4964575, upload-time = "2026-03-13T13:53:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/46/76/7d051671e938b1881670528fec69cc4044315edd71a229c7fd712eaa5119/fonttools-4.62.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069", size = 4953693, upload-time = "2026-03-13T13:53:26.569Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ae/b41f8628ec0be3c1b934fc12b84f4576a5c646119db4d3bdd76a217c90b5/fonttools-4.62.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9", size = 5094920, upload-time = "2026-03-13T13:53:29.329Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/53a1e9469331a23dcc400970a27a4caa3d9f6edbf5baab0260285238b884/fonttools-4.62.1-cp313-cp313-win32.whl", hash = "sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24", size = 2279928, upload-time = "2026-03-13T13:53:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/38/60/35186529de1db3c01f5ad625bde07c1f576305eab6d86bbda4c58445f721/fonttools-4.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056", size = 2330514, upload-time = "2026-03-13T13:53:34.991Z" }, + { url = "https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca", size = 2864442, upload-time = "2026-03-13T13:53:37.509Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b2/e521803081f8dc35990816b82da6360fa668a21b44da4b53fc9e77efcd62/fonttools-4.62.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca", size = 2410901, upload-time = "2026-03-13T13:53:40.55Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/8c3511ff06e53110039358dbbdc1a65d72157a054638387aa2ada300a8b8/fonttools-4.62.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782", size = 4999608, upload-time = "2026-03-13T13:53:42.798Z" }, + { url = "https://files.pythonhosted.org/packages/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae", size = 4912726, upload-time = "2026-03-13T13:53:45.405Z" }, + { url = "https://files.pythonhosted.org/packages/70/b9/ac677cb07c24c685cf34f64e140617d58789d67a3dd524164b63648c6114/fonttools-4.62.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7", size = 4951422, upload-time = "2026-03-13T13:53:48.326Z" }, + { url = "https://files.pythonhosted.org/packages/e6/10/11c08419a14b85b7ca9a9faca321accccc8842dd9e0b1c8a72908de05945/fonttools-4.62.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a", size = 5060979, upload-time = "2026-03-13T13:53:51.366Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3c/12eea4a4cf054e7ab058ed5ceada43b46809fce2bf319017c4d63ae55bb4/fonttools-4.62.1-cp314-cp314-win32.whl", hash = "sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800", size = 2283733, upload-time = "2026-03-13T13:53:53.606Z" }, + { url = "https://files.pythonhosted.org/packages/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.whl", hash = "sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e", size = 2335663, upload-time = "2026-03-13T13:53:56.23Z" }, + { url = "https://files.pythonhosted.org/packages/42/c5/4d2ed3ca6e33617fc5624467da353337f06e7f637707478903c785bd8e20/fonttools-4.62.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82", size = 2947288, upload-time = "2026-03-13T13:53:59.397Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e9/7ab11ddfda48ed0f89b13380e5595ba572619c27077be0b2c447a63ff351/fonttools-4.62.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260", size = 2449023, upload-time = "2026-03-13T13:54:01.642Z" }, + { url = "https://files.pythonhosted.org/packages/b2/10/a800fa090b5e8819942e54e19b55fc7c21fe14a08757c3aa3ca8db358939/fonttools-4.62.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4", size = 5137599, upload-time = "2026-03-13T13:54:04.495Z" }, + { url = "https://files.pythonhosted.org/packages/37/dc/8ccd45033fffd74deb6912fa1ca524643f584b94c87a16036855b498a1ed/fonttools-4.62.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b", size = 4920933, upload-time = "2026-03-13T13:54:07.557Z" }, + { url = "https://files.pythonhosted.org/packages/99/eb/e618adefb839598d25ac8136cd577925d6c513dc0d931d93b8af956210f0/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87", size = 5016232, upload-time = "2026-03-13T13:54:10.611Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5f/9b5c9bfaa8ec82def8d8168c4f13615990d6ce5996fe52bd49bfb5e05134/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c", size = 5042987, upload-time = "2026-03-13T13:54:13.569Z" }, + { url = "https://files.pythonhosted.org/packages/90/aa/dfbbe24c6a6afc5c203d90cc0343e24bcbb09e76d67c4d6eef8c2558d7ba/fonttools-4.62.1-cp314-cp314t-win32.whl", hash = "sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a", size = 2348021, upload-time = "2026-03-13T13:54:16.98Z" }, + { url = "https://files.pythonhosted.org/packages/13/6f/ae9c4e4dd417948407b680855c2c7790efb52add6009aaecff1e3bc50e8e/fonttools-4.62.1-cp314-cp314t-win_amd64.whl", hash = "sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e", size = 2414147, upload-time = "2026-03-13T13:54:19.416Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, +] + +[[package]] +name = "h5netcdf" +version = "1.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/03/92d6cc02c0055158167255980461155d6e17f1c4143c03f8bcc18d3e3f3a/h5netcdf-1.8.1.tar.gz", hash = "sha256:9b396a4cc346050fc1a4df8523bc1853681ec3544e0449027ae397cb953c7a16", size = 78679, upload-time = "2026-01-23T07:35:31.233Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/8b/88f16936a8e8070a83d36239555227ecd91728f9ef222c5382cda07e0fd6/h5netcdf-1.8.1-py3-none-any.whl", hash = "sha256:a76ed7cfc9b8a8908ea7057c4e57e27307acff1049b7f5ed52db6c2247636879", size = 62915, upload-time = "2026-01-23T07:35:30.195Z" }, +] + +[[package]] +name = "h5py" +version = "3.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/33/acd0ce6863b6c0d7735007df01815403f5589a21ff8c2e1ee2587a38f548/h5py-3.16.0.tar.gz", hash = "sha256:a0dbaad796840ccaa67a4c144a0d0c8080073c34c76d5a6941d6818678ef2738", size = 446526, upload-time = "2026-03-06T13:49:08.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/95/a825894f3e45cbac7554c4e97314ce886b233a20033787eda755ca8fecc7/h5py-3.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:719439d14b83f74eeb080e9650a6c7aa6d0d9ea0ca7f804347b05fac6fbf18af", size = 3721663, upload-time = "2026-03-06T13:47:49.599Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3b/38ff88b347c3e346cda1d3fc1b65a7aa75d40632228d8b8a5d7b58508c24/h5py-3.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c3f0a0e136f2e95dd0b67146abb6668af4f1a69c81ef8651a2d316e8e01de447", size = 3087630, upload-time = "2026-03-06T13:47:51.249Z" }, + { url = "https://files.pythonhosted.org/packages/98/a8/2594cef906aee761601eff842c7dc598bea2b394a3e1c00966832b8eeb7c/h5py-3.16.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a6fbc5367d4046801f9b7db9191b31895f22f1c6df1f9987d667854cac493538", size = 4823472, upload-time = "2026-03-06T13:47:53.085Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/c1f604538ff6db22a0690be2dc44ab59178e115f63c917794e529356ab23/h5py-3.16.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:fb1720028d99040792bb2fb31facb8da44a6f29df7697e0b84f0d79aff2e9bd3", size = 5027150, upload-time = "2026-03-06T13:47:55.043Z" }, + { url = "https://files.pythonhosted.org/packages/2e/fd/301739083c2fc4fd89950f9bcfce75d6e14b40b0ca3d40e48a8993d1722c/h5py-3.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:314b6054fe0b1051c2b0cb2df5cbdab15622fb05e80f202e3b6a5eee0d6fe365", size = 4814544, upload-time = "2026-03-06T13:47:56.893Z" }, + { url = "https://files.pythonhosted.org/packages/4c/42/2193ed41ccee78baba8fcc0cff2c925b8b9ee3793305b23e1f22c20bf4c7/h5py-3.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ffbab2fedd6581f6aa31cf1639ca2cb86e02779de525667892ebf4cc9fd26434", size = 5034013, upload-time = "2026-03-06T13:47:59.01Z" }, + { url = "https://files.pythonhosted.org/packages/f7/20/e6c0ff62ca2ad1a396a34f4380bafccaaf8791ff8fccf3d995a1fc12d417/h5py-3.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:17d1f1630f92ad74494a9a7392ab25982ce2b469fc62da6074c0ce48366a2999", size = 3191673, upload-time = "2026-03-06T13:48:00.626Z" }, + { url = "https://files.pythonhosted.org/packages/f2/48/239cbe352ac4f2b8243a8e620fa1a2034635f633731493a7ff1ed71e8658/h5py-3.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:85b9c49dd58dc44cf70af944784e2c2038b6f799665d0dcbbc812a26e0faa859", size = 2673834, upload-time = "2026-03-06T13:48:02.579Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c0/5d4119dba94093bbafede500d3defd2f5eab7897732998c04b54021e530b/h5py-3.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5313566f4643121a78503a473f0fb1e6dcc541d5115c44f05e037609c565c4d", size = 3685604, upload-time = "2026-03-06T13:48:04.198Z" }, + { url = "https://files.pythonhosted.org/packages/b0/42/c84efcc1d4caebafb1ecd8be4643f39c85c47a80fe254d92b8b43b1eadaf/h5py-3.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:42b012933a83e1a558c673176676a10ce2fd3759976a0fedee1e672d1e04fc9d", size = 3061940, upload-time = "2026-03-06T13:48:05.783Z" }, + { url = "https://files.pythonhosted.org/packages/89/84/06281c82d4d1686fde1ac6b0f307c50918f1c0151062445ab3b6fa5a921d/h5py-3.16.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:ff24039e2573297787c3063df64b60aab0591980ac898329a08b0320e0cf2527", size = 5198852, upload-time = "2026-03-06T13:48:07.482Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/1a19e42cd43cc1365e127db6aae85e1c671da1d9a5d746f4d34a50edb577/h5py-3.16.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:dfc21898ff025f1e8e67e194965a95a8d4754f452f83454538f98f8a3fcb207e", size = 5405250, upload-time = "2026-03-06T13:48:09.628Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/9790c1655eabeb85b92b1ecab7d7e62a2069e53baefd58c98f0909c7a948/h5py-3.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:698dd69291272642ffda44a0ecd6cd3bda5faf9621452d255f57ce91487b9794", size = 5190108, upload-time = "2026-03-06T13:48:11.26Z" }, + { url = "https://files.pythonhosted.org/packages/51/d7/ab693274f1bd7e8c5f9fdd6c7003a88d59bedeaf8752716a55f532924fbb/h5py-3.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2b2c02b0a160faed5fb33f1ba8a264a37ee240b22e049ecc827345d0d9043074", size = 5419216, upload-time = "2026-03-06T13:48:13.322Z" }, + { url = "https://files.pythonhosted.org/packages/03/c1/0976b235cf29ead553e22f2fb6385a8252b533715e00d0ae52ed7b900582/h5py-3.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:96b422019a1c8975c2d5dadcf61d4ba6f01c31f92bbde6e4649607885fe502d6", size = 3182868, upload-time = "2026-03-06T13:48:15.759Z" }, + { url = "https://files.pythonhosted.org/packages/14/d9/866b7e570b39070f92d47b0ff1800f0f8239b6f9e45f02363d7112336c1f/h5py-3.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:39c2838fb1e8d97bcf1755e60ad1f3dd76a7b2a475928dc321672752678b96db", size = 2653286, upload-time = "2026-03-06T13:48:17.279Z" }, + { url = "https://files.pythonhosted.org/packages/0f/9e/6142ebfda0cb6e9349c091eae73c2e01a770b7659255248d637bec54a88b/h5py-3.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:370a845f432c2c9619db8eed334d1e610c6015796122b0e57aa46312c22617d9", size = 3671808, upload-time = "2026-03-06T13:48:19.737Z" }, + { url = "https://files.pythonhosted.org/packages/b0/65/5e088a45d0f43cd814bc5bec521c051d42005a472e804b1a36c48dada09b/h5py-3.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42108e93326c50c2810025aade9eac9d6827524cdccc7d4b75a546e5ab308edb", size = 3045837, upload-time = "2026-03-06T13:48:21.854Z" }, + { url = "https://files.pythonhosted.org/packages/da/1e/6172269e18cc5a484e2913ced33339aad588e02ba407fafd00d369e22ef3/h5py-3.16.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:099f2525c9dcf28de366970a5fb34879aab20491589fa89ce2863a84218bb524", size = 5193860, upload-time = "2026-03-06T13:48:24.071Z" }, + { url = "https://files.pythonhosted.org/packages/bd/98/ef2b6fe2903e377cbe870c3b2800d62552f1e3dbe81ce49e1923c53d1c5c/h5py-3.16.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9300ad32dea9dfc5171f94d5f6948e159ed93e4701280b0f508773b3f582f402", size = 5400417, upload-time = "2026-03-06T13:48:25.728Z" }, + { url = "https://files.pythonhosted.org/packages/bc/81/5b62d760039eed64348c98129d17061fdfc7839fc9c04eaaad6dee1004e4/h5py-3.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:171038f23bccddfc23f344cadabdfc9917ff554db6a0d417180d2747fe4c75a7", size = 5185214, upload-time = "2026-03-06T13:48:27.436Z" }, + { url = "https://files.pythonhosted.org/packages/28/c4/532123bcd9080e250696779c927f2cb906c8bf3447df98f5ceb8dcded539/h5py-3.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7e420b539fb6023a259a1b14d4c9f6df8cf50d7268f48e161169987a57b737ff", size = 5414598, upload-time = "2026-03-06T13:48:29.49Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d9/a27997f84341fc0dfcdd1fe4179b6ba6c32a7aa880fdb8c514d4dad6fba3/h5py-3.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:18f2bbcd545e6991412253b98727374c356d67caa920e68dc79eab36bf5fedad", size = 3175509, upload-time = "2026-03-06T13:48:31.131Z" }, + { url = "https://files.pythonhosted.org/packages/a5/23/bb8647521d4fd770c30a76cfc6cb6a2f5495868904054e92f2394c5a78ff/h5py-3.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:656f00e4d903199a1d58df06b711cf3ca632b874b4207b7dbec86185b5c8c7d4", size = 2647362, upload-time = "2026-03-06T13:48:33.411Z" }, + { url = "https://files.pythonhosted.org/packages/48/3c/7fcd9b4c9eed82e91fb15568992561019ae7a829d1f696b2c844355d95dd/h5py-3.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9c9d307c0ef862d1cd5714f72ecfafe0a5d7529c44845afa8de9f46e5ba8bd65", size = 3678608, upload-time = "2026-03-06T13:48:35.183Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b7/9366ed44ced9b7ef357ab48c94205280276db9d7f064aa3012a97227e966/h5py-3.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8c1eff849cdd53cbc73c214c30ebdb6f1bb8b64790b4b4fc36acdb5e43570210", size = 3054773, upload-time = "2026-03-06T13:48:37.139Z" }, + { url = "https://files.pythonhosted.org/packages/58/a5/4964bc0e91e86340c2bbda83420225b2f770dcf1eb8a39464871ad769436/h5py-3.16.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:e2c04d129f180019e216ee5f9c40b78a418634091c8782e1f723a6ca3658b965", size = 5198886, upload-time = "2026-03-06T13:48:38.879Z" }, + { url = "https://files.pythonhosted.org/packages/f1/16/d905e7f53e661ce2c24686c38048d8e2b750ffc4350009d41c4e6c6c9826/h5py-3.16.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:e4360f15875a532bc7b98196c7592ed4fc92672a57c0a621355961cafb17a6dd", size = 5404883, upload-time = "2026-03-06T13:48:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f2/58f34cb74af46d39f4cd18ea20909a8514960c5a3e5b92fd06a28161e0a8/h5py-3.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3fae9197390c325e62e0a1aa977f2f62d994aa87aab182abbea85479b791197c", size = 5192039, upload-time = "2026-03-06T13:48:43.117Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ca/934a39c24ce2e2db017268c08da0537c20fa0be7e1549be3e977313fc8f5/h5py-3.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:43259303989ac8adacc9986695b31e35dba6fd1e297ff9c6a04b7da5542139cc", size = 5421526, upload-time = "2026-03-06T13:48:44.838Z" }, + { url = "https://files.pythonhosted.org/packages/3e/14/615a450205e1b56d16c6783f5ccd116cde05550faad70ae077c955654a75/h5py-3.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:fa48993a0b799737ba7fd21e2350fa0a60701e58180fae9f2de834bc39a147ab", size = 3183263, upload-time = "2026-03-06T13:48:47.117Z" }, + { url = "https://files.pythonhosted.org/packages/7b/48/a6faef5ed632cae0c65ac6b214a6614a0b510c3183532c521bdb0055e117/h5py-3.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:1897a771a7f40d05c262fc8f37376ec37873218544b70216872876c627640f63", size = 2663450, upload-time = "2026-03-06T13:48:48.707Z" }, + { url = "https://files.pythonhosted.org/packages/5d/32/0c8bb8aedb62c772cf7c1d427c7d1951477e8c2835f872bc0a13d1f85f86/h5py-3.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:15922e485844f77c0b9d275396d435db3baa58292a9c2176a386e072e0cf2491", size = 3760693, upload-time = "2026-03-06T13:48:50.453Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1f/fcc5977d32d6387c5c9a694afee716a5e20658ac08b3ff24fdec79fb05f2/h5py-3.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:df02dd29bd247f98674634dfe41f89fd7c16ba3d7de8695ec958f58404a4e618", size = 3181305, upload-time = "2026-03-06T13:48:52.221Z" }, + { url = "https://files.pythonhosted.org/packages/f5/a1/af87f64b9f986889884243643621ebbd4ac72472ba8ec8cec891ac8e2ca1/h5py-3.16.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:0f456f556e4e2cebeebd9d66adf8dc321770a42593494a0b6f0af54a7567b242", size = 5074061, upload-time = "2026-03-06T13:48:54.089Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d0/146f5eaff3dc246a9c7f6e5e4f42bd45cc613bce16693bcd4d1f7c958bf5/h5py-3.16.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:3e6cb3387c756de6a9492d601553dffea3fe11b5f22b443aac708c69f3f55e16", size = 5279216, upload-time = "2026-03-06T13:48:56.75Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9d/12a13424f1e604fc7df9497b73c0356fb78c2fb206abd7465ce47226e8fd/h5py-3.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8389e13a1fd745ad2856873e8187fd10268b2d9677877bb667b41aebd771d8b7", size = 5070068, upload-time = "2026-03-06T13:48:59.169Z" }, + { url = "https://files.pythonhosted.org/packages/41/8c/bbe98f813722b4873818a8db3e15aa3e625b59278566905ac439725e8070/h5py-3.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:346df559a0f7dcb31cf8e44805319e2ab24b8957c45e7708ce503b2ec79ba725", size = 5300253, upload-time = "2026-03-06T13:49:02.033Z" }, + { url = "https://files.pythonhosted.org/packages/32/9e/87e6705b4d6890e7cecdf876e2a7d3e40654a2ae37482d79a6f1b87f7b92/h5py-3.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4c6ab014ab704b4feaa719ae783b86522ed0bf1f82184704ed3c9e4e3228796e", size = 3381671, upload-time = "2026-03-06T13:49:04.351Z" }, + { url = "https://files.pythonhosted.org/packages/96/91/9fad90cfc5f9b2489c7c26ad897157bce82f0e9534a986a221b99760b23b/h5py-3.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:faca8fb4e4319c09d83337adc80b2ca7d5c5a343c2d6f1b6388f32cfecca13c1", size = 2740706, upload-time = "2026-03-06T13:49:06.347Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "instrumentserver" +version = "0.0.1" +source = { editable = "." } +dependencies = [ + { name = "pyqt5" }, + { name = "pyzmq" }, + { name = "qcodes" }, + { name = "qtpy" }, + { name = "scipy" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-qt" }, +] + +[package.metadata] +requires-dist = [ + { name = "pyqt5" }, + { name = "pyzmq" }, + { name = "qcodes" }, + { name = "qtpy" }, + { name = "scipy" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=9.0.2" }, + { name = "pytest-qt", specifier = ">=4.5.0" }, +] + +[[package]] +name = "ipykernel" +version = "7.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appnope", marker = "sys_platform == 'darwin'" }, + { name = "comm" }, + { name = "debugpy" }, + { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "ipython", version = "9.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "matplotlib-inline" }, + { name = "nest-asyncio" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/8d/b68b728e2d06b9e0051019640a40a9eb7a88fcd82c2e1b5ce70bef5ff044/ipykernel-7.2.0.tar.gz", hash = "sha256:18ed160b6dee2cbb16e5f3575858bc19d8f1fe6046a9a680c708494ce31d909e", size = 176046, upload-time = "2026-02-06T16:43:27.403Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl", hash = "sha256:3bbd4420d2b3cc105cbdf3756bfc04500b1e52f090a90716851f3916c62e1661", size = 118788, upload-time = "2026-02-06T16:43:25.149Z" }, +] + +[[package]] +name = "ipython" +version = "9.10.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version < '3.12' and sys_platform == 'emscripten'", + "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.12' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version < '3.12'" }, + { name = "ipython-pygments-lexers", marker = "python_full_version < '3.12'" }, + { name = "jedi", marker = "python_full_version < '3.12'" }, + { name = "matplotlib-inline", marker = "python_full_version < '3.12'" }, + { name = "pexpect", marker = "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version < '3.12'" }, + { name = "pygments", marker = "python_full_version < '3.12'" }, + { name = "stack-data", marker = "python_full_version < '3.12'" }, + { name = "traitlets", marker = "python_full_version < '3.12'" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/60/2111715ea11f39b1535bed6024b7dec7918b71e5e5d30855a5b503056b50/ipython-9.10.0.tar.gz", hash = "sha256:cd9e656be97618a0676d058134cd44e6dc7012c0e5cb36a9ce96a8c904adaf77", size = 4426526, upload-time = "2026-02-02T10:00:33.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/aa/898dec789a05731cd5a9f50605b7b44a72bd198fd0d4528e11fc610177cc/ipython-9.10.0-py3-none-any.whl", hash = "sha256:c6ab68cc23bba8c7e18e9b932797014cc61ea7fd6f19de180ab9ba73e65ee58d", size = 622774, upload-time = "2026-02-02T10:00:31.503Z" }, +] + +[[package]] +name = "ipython" +version = "9.11.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version >= '3.12'" }, + { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.12'" }, + { name = "jedi", marker = "python_full_version >= '3.12'" }, + { name = "matplotlib-inline", marker = "python_full_version >= '3.12'" }, + { name = "pexpect", marker = "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version >= '3.12'" }, + { name = "pygments", marker = "python_full_version >= '3.12'" }, + { name = "stack-data", marker = "python_full_version >= '3.12'" }, + { name = "traitlets", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/28/a4698eda5a8928a45d6b693578b135b753e14fa1c2b36ee9441e69a45576/ipython-9.11.0.tar.gz", hash = "sha256:2a94bc4406b22ecc7e4cb95b98450f3ea493a76bec8896cda11b78d7752a6667", size = 4427354, upload-time = "2026-03-05T08:57:30.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/90/45c72becc57158facc6a6404f663b77bbcea2519ca57f760e2879ae1315d/ipython-9.11.0-py3-none-any.whl", hash = "sha256:6922d5bcf944c6e525a76a0a304451b60a2b6f875e86656d8bc2dfda5d710e19", size = 624222, upload-time = "2026-03-05T08:57:28.94Z" }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, +] + +[[package]] +name = "ipywidgets" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "comm" }, + { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "ipython", version = "9.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "jupyterlab-widgets" }, + { name = "traitlets" }, + { name = "widgetsnbextension" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/ae/c5ce1edc1afe042eadb445e95b0671b03cee61895264357956e61c0d2ac0/ipywidgets-8.1.8.tar.gz", hash = "sha256:61f969306b95f85fba6b6986b7fe45d73124d1d9e3023a8068710d47a22ea668", size = 116739, upload-time = "2025-11-01T21:18:12.393Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl", hash = "sha256:ecaca67aed704a338f88f67b1181b58f821ab5dc89c1f0f5ef99db43c1c2921e", size = 139808, upload-time = "2025-11-01T21:18:10.956Z" }, +] + +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "jupyter-client" +version = "8.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-core" }, + { name = "python-dateutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/e4/ba649102a3bc3fbca54e7239fb924fd434c766f855693d86de0b1f2bec81/jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e", size = 348020, upload-time = "2026-01-08T13:55:47.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl", hash = "sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a", size = 107371, upload-time = "2026-01-08T13:55:45.562Z" }, +] + +[[package]] +name = "jupyter-core" +version = "5.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, +] + +[[package]] +name = "jupyterlab-widgets" +version = "3.0.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/2d/ef58fed122b268c69c0aa099da20bc67657cdfb2e222688d5731bd5b971d/jupyterlab_widgets-3.0.16.tar.gz", hash = "sha256:423da05071d55cf27a9e602216d35a3a65a3e41cdf9c5d3b643b814ce38c19e0", size = 897423, upload-time = "2025-11-01T21:11:29.724Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl", hash = "sha256:45fa36d9c6422cf2559198e4db481aa243c7a32d9926b500781c830c80f7ecf8", size = 914926, upload-time = "2025-11-01T21:11:28.008Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/dd/a495a9c104be1c476f0386e714252caf2b7eca883915422a64c50b88c6f5/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9eed0f7edbb274413b6ee781cca50541c8c0facd3d6fd289779e494340a2b85c", size = 122798, upload-time = "2026-03-09T13:12:58.963Z" }, + { url = "https://files.pythonhosted.org/packages/11/60/37b4047a2af0cf5ef6d8b4b26e91829ae6fc6a2d1f74524bcb0e7cd28a32/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb", size = 66216, upload-time = "2026-03-09T13:13:00.155Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/510dc933d87767584abfe03efa445889996c70c2990f6f87c3ebaa0a18c5/kiwisolver-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac", size = 63911, upload-time = "2026-03-09T13:13:01.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/46/bddc13df6c2a40741e0cc7865bb1c9ed4796b6760bd04ce5fae3928ef917/kiwisolver-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27", size = 1438209, upload-time = "2026-03-09T13:13:03.385Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d6/76621246f5165e5372f02f5e6f3f48ea336a8f9e96e43997d45b240ed8cd/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398", size = 1248888, upload-time = "2026-03-09T13:13:05.231Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c1/31559ec6fb39a5b48035ce29bb63ade628f321785f38c384dee3e2c08bc1/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6176c1811d9d5a04fa391c490cc44f451e240697a16977f11c6f722efb9041db", size = 1266304, upload-time = "2026-03-09T13:13:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ef/1cb8276f2d29cc6a41e0a042f27946ca347d3a4a75acf85d0a16aa6dcc82/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50847dca5d197fcbd389c805aa1a1cf32f25d2e7273dc47ab181a517666b68cc", size = 1319650, upload-time = "2026-03-09T13:13:08.607Z" }, + { url = "https://files.pythonhosted.org/packages/4c/e4/5ba3cecd7ce6236ae4a80f67e5d5531287337d0e1f076ca87a5abe4cd5d0/kiwisolver-1.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679", size = 970949, upload-time = "2026-03-09T13:13:10.299Z" }, + { url = "https://files.pythonhosted.org/packages/5a/69/dc61f7ae9a2f071f26004ced87f078235b5507ab6e5acd78f40365655034/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1f9f4121ec58628c96baa3de1a55a4e3a333c5102c8e94b64e23bf7b2083309", size = 2199125, upload-time = "2026-03-09T13:13:11.841Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7b/abbe0f1b5afa85f8d084b73e90e5f801c0939eba16ac2e49af7c61a6c28d/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b7d335370ae48a780c6e6a6bbfa97342f563744c39c35562f3f367665f5c1de2", size = 2293783, upload-time = "2026-03-09T13:13:14.399Z" }, + { url = "https://files.pythonhosted.org/packages/8a/80/5908ae149d96d81580d604c7f8aefd0e98f4fd728cf172f477e9f2a81744/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:800ee55980c18545af444d93fdd60c56b580db5cc54867d8cbf8a1dc0829938c", size = 1960726, upload-time = "2026-03-09T13:13:16.047Z" }, + { url = "https://files.pythonhosted.org/packages/84/08/a78cb776f8c085b7143142ce479859cfec086bd09ee638a317040b6ef420/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c438f6ca858697c9ab67eb28246c92508af972e114cac34e57a6d4ba17a3ac08", size = 2464738, upload-time = "2026-03-09T13:13:17.897Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e1/65584da5356ed6cb12c63791a10b208860ac40a83de165cb6a6751a686e3/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c63c91f95173f9c2a67c7c526b2cea976828a0e7fced9cdcead2802dc10f8a4", size = 2270718, upload-time = "2026-03-09T13:13:19.421Z" }, + { url = "https://files.pythonhosted.org/packages/be/6c/28f17390b62b8f2f520e2915095b3c94d88681ecf0041e75389d9667f202/kiwisolver-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b", size = 73480, upload-time = "2026-03-09T13:13:20.818Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0e/2ee5debc4f77a625778fec5501ff3e8036fe361b7ee28ae402a485bb9694/kiwisolver-1.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:ad4ae4ffd1ee9cd11357b4c66b612da9888f4f4daf2f36995eda64bd45370cac", size = 64930, upload-time = "2026-03-09T13:13:21.997Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b2/818b74ebea34dabe6d0c51cb1c572e046730e64844da6ed646d5298c40ce/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9", size = 123158, upload-time = "2026-03-09T13:13:23.127Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d9/405320f8077e8e1c5c4bd6adc45e1e6edf6d727b6da7f2e2533cf58bff71/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588", size = 66388, upload-time = "2026-03-09T13:13:24.765Z" }, + { url = "https://files.pythonhosted.org/packages/99/9f/795fedf35634f746151ca8839d05681ceb6287fbed6cc1c9bf235f7887c2/kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819", size = 64068, upload-time = "2026-03-09T13:13:25.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f", size = 1477934, upload-time = "2026-03-09T13:13:27.166Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2f/cebfcdb60fd6a9b0f6b47a9337198bcbad6fbe15e68189b7011fd914911f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf", size = 1278537, upload-time = "2026-03-09T13:13:28.707Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0d/9b782923aada3fafb1d6b84e13121954515c669b18af0c26e7d21f579855/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d", size = 1296685, upload-time = "2026-03-09T13:13:30.528Z" }, + { url = "https://files.pythonhosted.org/packages/27/70/83241b6634b04fe44e892688d5208332bde130f38e610c0418f9ede47ded/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083", size = 1346024, upload-time = "2026-03-09T13:13:32.818Z" }, + { url = "https://files.pythonhosted.org/packages/e4/db/30ed226fb271ae1a6431fc0fe0edffb2efe23cadb01e798caeb9f2ceae8f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6", size = 987241, upload-time = "2026-03-09T13:13:34.435Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bd/c314595208e4c9587652d50959ead9e461995389664e490f4dce7ff0f782/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1", size = 2227742, upload-time = "2026-03-09T13:13:36.4Z" }, + { url = "https://files.pythonhosted.org/packages/c1/43/0499cec932d935229b5543d073c2b87c9c22846aab48881e9d8d6e742a2d/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0", size = 2323966, upload-time = "2026-03-09T13:13:38.204Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6f/79b0d760907965acfd9d61826a3d41f8f093c538f55cd2633d3f0db269f6/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15", size = 1977417, upload-time = "2026-03-09T13:13:39.966Z" }, + { url = "https://files.pythonhosted.org/packages/ab/31/01d0537c41cb75a551a438c3c7a80d0c60d60b81f694dac83dd436aec0d0/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314", size = 2491238, upload-time = "2026-03-09T13:13:41.698Z" }, + { url = "https://files.pythonhosted.org/packages/e4/34/8aefdd0be9cfd00a44509251ba864f5caf2991e36772e61c408007e7f417/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9", size = 2294947, upload-time = "2026-03-09T13:13:43.343Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/0348374369ca588f8fe9c338fae49fa4e16eeb10ffb3d012f23a54578a9e/kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384", size = 73569, upload-time = "2026-03-09T13:13:45.792Z" }, + { url = "https://files.pythonhosted.org/packages/28/26/192b26196e2316e2bd29deef67e37cdf9870d9af8e085e521afff0fed526/kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7", size = 64997, upload-time = "2026-03-09T13:13:46.878Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/024d6711d5ba575aa65d5538042e99964104e97fa153a9f10bc369182bc2/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09", size = 123166, upload-time = "2026-03-09T13:13:48.032Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3", size = 66395, upload-time = "2026-03-09T13:13:49.365Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd", size = 64065, upload-time = "2026-03-09T13:13:50.562Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3", size = 1477903, upload-time = "2026-03-09T13:13:52.084Z" }, + { url = "https://files.pythonhosted.org/packages/18/d8/55638d89ffd27799d5cc3d8aa28e12f4ce7a64d67b285114dbedc8ea4136/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96", size = 1278751, upload-time = "2026-03-09T13:13:54.673Z" }, + { url = "https://files.pythonhosted.org/packages/b8/97/b4c8d0d18421ecceba20ad8701358453b88e32414e6f6950b5a4bad54e65/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099", size = 1296793, upload-time = "2026-03-09T13:13:56.287Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/f862f94b6389d8957448ec9df59450b81bec4abb318805375c401a1e6892/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8", size = 1346041, upload-time = "2026-03-09T13:13:58.269Z" }, + { url = "https://files.pythonhosted.org/packages/a3/6a/f1650af35821eaf09de398ec0bc2aefc8f211f0cda50204c9f1673741ba9/kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87", size = 987292, upload-time = "2026-03-09T13:13:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/de/19/d7fb82984b9238115fe629c915007be608ebd23dc8629703d917dbfaffd4/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23", size = 2227865, upload-time = "2026-03-09T13:14:01.401Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/46b7f386589fd222dac9e9de9c956ce5bcefe2ee73b4e79891381dda8654/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859", size = 2324369, upload-time = "2026-03-09T13:14:02.972Z" }, + { url = "https://files.pythonhosted.org/packages/92/8b/95e237cf3d9c642960153c769ddcbe278f182c8affb20cecc1cc983e7cc5/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902", size = 1977989, upload-time = "2026-03-09T13:14:04.503Z" }, + { url = "https://files.pythonhosted.org/packages/1b/95/980c9df53501892784997820136c01f62bc1865e31b82b9560f980c0e649/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167", size = 2491645, upload-time = "2026-03-09T13:14:06.106Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/900647fd0840abebe1561792c6b31e6a7c0e278fc3973d30572a965ca14c/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0", size = 2295237, upload-time = "2026-03-09T13:14:08.891Z" }, + { url = "https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276", size = 73573, upload-time = "2026-03-09T13:14:12.327Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d2/64be2e429eb4fca7f7e1c52a91b12663aeaf25de3895e5cca0f47ef2a8d0/kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c", size = 64998, upload-time = "2026-03-09T13:14:13.469Z" }, + { url = "https://files.pythonhosted.org/packages/b0/69/ce68dd0c85755ae2de490bf015b62f2cea5f6b14ff00a463f9d0774449ff/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1", size = 125700, upload-time = "2026-03-09T13:14:14.636Z" }, + { url = "https://files.pythonhosted.org/packages/74/aa/937aac021cf9d4349990d47eb319309a51355ed1dbdc9c077cdc9224cb11/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e", size = 67537, upload-time = "2026-03-09T13:14:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ee/20/3a87fbece2c40ad0f6f0aefa93542559159c5f99831d596050e8afae7a9f/kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7", size = 65514, upload-time = "2026-03-09T13:14:18.035Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7f/f943879cda9007c45e1f7dba216d705c3a18d6b35830e488b6c6a4e7cdf0/kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c", size = 1584848, upload-time = "2026-03-09T13:14:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/37/f8/4d4f85cc1870c127c88d950913370dd76138482161cd07eabbc450deff01/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368", size = 1391542, upload-time = "2026-03-09T13:14:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/04/0b/65dd2916c84d252b244bd405303220f729e7c17c9d7d33dca6feeff9ffc4/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489", size = 1404447, upload-time = "2026-03-09T13:14:23.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/2606a373247babce9b1d056c03a04b65f3cf5290a8eac5d7bdead0a17e21/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1", size = 1455918, upload-time = "2026-03-09T13:14:24.74Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d1/c6078b5756670658e9192a2ef11e939c92918833d2745f85cd14a6004bdf/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3", size = 1072856, upload-time = "2026-03-09T13:14:26.597Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c8/7def6ddf16eb2b3741d8b172bdaa9af882b03c78e9b0772975408801fa63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18", size = 2333580, upload-time = "2026-03-09T13:14:28.237Z" }, + { url = "https://files.pythonhosted.org/packages/9e/87/2ac1fce0eb1e616fcd3c35caa23e665e9b1948bb984f4764790924594128/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021", size = 2423018, upload-time = "2026-03-09T13:14:30.018Z" }, + { url = "https://files.pythonhosted.org/packages/67/13/c6700ccc6cc218716bfcda4935e4b2997039869b4ad8a94f364c5a3b8e63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310", size = 2062804, upload-time = "2026-03-09T13:14:32.888Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482, upload-time = "2026-03-09T13:14:34.971Z" }, + { url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328, upload-time = "2026-03-09T13:14:36.816Z" }, + { url = "https://files.pythonhosted.org/packages/47/84/6a6d5e5bb8273756c27b7d810d47f7ef2f1f9b9fd23c9ee9a3f8c75c9cef/kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53", size = 68410, upload-time = "2026-03-09T13:14:38.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/060f45052f2a01ad5762c8fdecd6d7a752b43400dc29ff75cd47225a40fd/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615", size = 123231, upload-time = "2026-03-09T13:14:41.323Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02", size = 66489, upload-time = "2026-03-09T13:14:42.534Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e", size = 64063, upload-time = "2026-03-09T13:14:44.759Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913, upload-time = "2026-03-09T13:14:46.247Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782, upload-time = "2026-03-09T13:14:48.458Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815, upload-time = "2026-03-09T13:14:50.721Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925, upload-time = "2026-03-09T13:14:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322, upload-time = "2026-03-09T13:14:54.637Z" }, + { url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857, upload-time = "2026-03-09T13:14:56.471Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376, upload-time = "2026-03-09T13:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549, upload-time = "2026-03-09T13:14:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680, upload-time = "2026-03-09T13:15:01.364Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905, upload-time = "2026-03-09T13:15:03.925Z" }, + { url = "https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e", size = 75086, upload-time = "2026-03-09T13:15:07.775Z" }, + { url = "https://files.pythonhosted.org/packages/70/15/9b90f7df0e31a003c71649cf66ef61c3c1b862f48c81007fa2383c8bd8d7/kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df", size = 66577, upload-time = "2026-03-09T13:15:09.139Z" }, + { url = "https://files.pythonhosted.org/packages/17/01/7dc8c5443ff42b38e72731643ed7cf1ed9bf01691ae5cdca98501999ed83/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e", size = 125794, upload-time = "2026-03-09T13:15:10.525Z" }, + { url = "https://files.pythonhosted.org/packages/46/8a/b4ebe46ebaac6a303417fab10c2e165c557ddaff558f9699d302b256bc53/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4", size = 67646, upload-time = "2026-03-09T13:15:12.016Z" }, + { url = "https://files.pythonhosted.org/packages/60/35/10a844afc5f19d6f567359bf4789e26661755a2f36200d5d1ed8ad0126e5/kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028", size = 65511, upload-time = "2026-03-09T13:15:13.311Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858, upload-time = "2026-03-09T13:15:15.103Z" }, + { url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539, upload-time = "2026-03-09T13:15:16.661Z" }, + { url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310, upload-time = "2026-03-09T13:15:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244, upload-time = "2026-03-09T13:15:20.444Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154, upload-time = "2026-03-09T13:15:22.039Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377, upload-time = "2026-03-09T13:15:23.741Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288, upload-time = "2026-03-09T13:15:25.789Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158, upload-time = "2026-03-09T13:15:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260, upload-time = "2026-03-09T13:15:29.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403, upload-time = "2026-03-09T13:15:31.517Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b5/98222136d839b8afabcaa943b09bd05888c2d36355b7e448550211d1fca4/kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681", size = 79687, upload-time = "2026-03-09T13:15:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/99/a2/ca7dc962848040befed12732dff6acae7fb3c4f6fc4272b3f6c9a30b8713/kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57", size = 70032, upload-time = "2026-03-09T13:15:34.411Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fa/2910df836372d8761bb6eff7d8bdcb1613b5c2e03f260efe7abe34d388a7/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797", size = 130262, upload-time = "2026-03-09T13:15:35.629Z" }, + { url = "https://files.pythonhosted.org/packages/0f/41/c5f71f9f00aabcc71fee8b7475e3f64747282580c2fe748961ba29b18385/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203", size = 138036, upload-time = "2026-03-09T13:15:36.894Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/7399a607f434119c6e1fdc8ec89a8d51ccccadf3341dee4ead6bd14caaf5/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7", size = 194295, upload-time = "2026-03-09T13:15:38.22Z" }, + { url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" }, + { url = "https://files.pythonhosted.org/packages/e9/eb/5fcbbbf9a0e2c3a35effb88831a483345326bbc3a030a3b5b69aee647f84/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ec4c85dc4b687c7f7f15f553ff26a98bfe8c58f5f7f0ac8905f0ba4c7be60232", size = 59532, upload-time = "2026-03-09T13:15:47.047Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9b/e17104555bb4db148fd52327feea1e96be4b88e8e008b029002c281a21ab/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:12e91c215a96e39f57989c8912ae761286ac5a9584d04030ceb3368a357f017a", size = 57420, upload-time = "2026-03-09T13:15:48.199Z" }, + { url = "https://files.pythonhosted.org/packages/48/44/2b5b95b7aa39fb2d8d9d956e0f3d5d45aef2ae1d942d4c3ffac2f9cfed1a/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be4a51a55833dc29ab5d7503e7bcb3b3af3402d266018137127450005cdfe737", size = 79892, upload-time = "2026-03-09T13:15:49.694Z" }, + { url = "https://files.pythonhosted.org/packages/52/7d/7157f9bba6b455cfb4632ed411e199fc8b8977642c2b12082e1bd9e6d173/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daae526907e262de627d8f70058a0f64acc9e2641c164c99c8f594b34a799a16", size = 77603, upload-time = "2026-03-09T13:15:50.945Z" }, + { url = "https://files.pythonhosted.org/packages/0a/dd/8050c947d435c8d4bc94e3252f4d8bb8a76cfb424f043a8680be637a57f1/kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1", size = 73558, upload-time = "2026-03-09T13:15:52.112Z" }, +] + +[[package]] +name = "locket" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/83/97b29fe05cb6ae28d2dbd30b81e2e402a3eed5f460c26e9eaa5895ceacf5/locket-1.0.0.tar.gz", hash = "sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632", size = 4350, upload-time = "2022-04-20T22:04:44.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl", hash = "sha256:b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3", size = 4398, upload-time = "2022-04-20T22:04:42.23Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215, upload-time = "2025-12-10T22:55:16.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625, upload-time = "2025-12-10T22:55:17.712Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614, upload-time = "2025-12-10T22:55:20.8Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997, upload-time = "2025-12-10T22:55:23.258Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825, upload-time = "2025-12-10T22:55:25.217Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090, upload-time = "2025-12-10T22:55:27.162Z" }, + { url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377, upload-time = "2025-12-10T22:55:29.185Z" }, + { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" }, + { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" }, + { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076, upload-time = "2025-12-10T22:55:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794, upload-time = "2025-12-10T22:55:46.252Z" }, + { url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474, upload-time = "2025-12-10T22:55:47.864Z" }, + { url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637, upload-time = "2025-12-10T22:55:50.048Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678, upload-time = "2025-12-10T22:55:52.21Z" }, + { url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686, upload-time = "2025-12-10T22:55:54.253Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917, upload-time = "2025-12-10T22:55:56.268Z" }, + { url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679, upload-time = "2025-12-10T22:55:57.856Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336, upload-time = "2025-12-10T22:55:59.371Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653, upload-time = "2025-12-10T22:56:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356, upload-time = "2025-12-10T22:56:02.95Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" }, + { url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" }, + { url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" }, + { url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" }, + { url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" }, + { url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198, upload-time = "2025-12-10T22:56:45.584Z" }, + { url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817, upload-time = "2025-12-10T22:56:47.339Z" }, + { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/10/8b/c265f4823726ab832de836cdd184d0986dcf94480f81e8739692a7ac7af2/numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd", size = 20727743, upload-time = "2026-03-09T07:58:53.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/51/5093a2df15c4dc19da3f79d1021e891f5dcf1d9d1db6ba38891d5590f3fe/numpy-2.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:33b3bf58ee84b172c067f56aeadc7ee9ab6de69c5e800ab5b10295d54c581adb", size = 16957183, upload-time = "2026-03-09T07:55:57.774Z" }, + { url = "https://files.pythonhosted.org/packages/b5/7c/c061f3de0630941073d2598dc271ac2f6cbcf5c83c74a5870fea07488333/numpy-2.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8ba7b51e71c05aa1f9bc3641463cd82308eab40ce0d5c7e1fd4038cbf9938147", size = 14968734, upload-time = "2026-03-09T07:56:00.494Z" }, + { url = "https://files.pythonhosted.org/packages/ef/27/d26c85cbcd86b26e4f125b0668e7a7c0542d19dd7d23ee12e87b550e95b5/numpy-2.4.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1988292870c7cb9d0ebb4cc96b4d447513a9644801de54606dc7aabf2b7d920", size = 5475288, upload-time = "2026-03-09T07:56:02.857Z" }, + { url = "https://files.pythonhosted.org/packages/2b/09/3c4abbc1dcd8010bf1a611d174c7aa689fc505585ec806111b4406f6f1b1/numpy-2.4.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:23b46bb6d8ecb68b58c09944483c135ae5f0e9b8d8858ece5e4ead783771d2a9", size = 6805253, upload-time = "2026-03-09T07:56:04.53Z" }, + { url = "https://files.pythonhosted.org/packages/21/bc/e7aa3f6817e40c3f517d407742337cbb8e6fc4b83ce0b55ab780c829243b/numpy-2.4.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a016db5c5dba78fa8fe9f5d80d6708f9c42ab087a739803c0ac83a43d686a470", size = 15969479, upload-time = "2026-03-09T07:56:06.638Z" }, + { url = "https://files.pythonhosted.org/packages/78/51/9f5d7a41f0b51649ddf2f2320595e15e122a40610b233d51928dd6c92353/numpy-2.4.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:715de7f82e192e8cae5a507a347d97ad17598f8e026152ca97233e3666daaa71", size = 16901035, upload-time = "2026-03-09T07:56:09.405Z" }, + { url = "https://files.pythonhosted.org/packages/64/6e/b221dd847d7181bc5ee4857bfb026182ef69499f9305eb1371cbb1aea626/numpy-2.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2ddb7919366ee468342b91dea2352824c25b55814a987847b6c52003a7c97f15", size = 17325657, upload-time = "2026-03-09T07:56:12.067Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b8/8f3fd2da596e1063964b758b5e3c970aed1949a05200d7e3d46a9d46d643/numpy-2.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a315e5234d88067f2d97e1f2ef670a7569df445d55400f1e33d117418d008d52", size = 18635512, upload-time = "2026-03-09T07:56:14.629Z" }, + { url = "https://files.pythonhosted.org/packages/5c/24/2993b775c37e39d2f8ab4125b44337ab0b2ba106c100980b7c274a22bee7/numpy-2.4.3-cp311-cp311-win32.whl", hash = "sha256:2b3f8d2c4589b1a2028d2a770b0fc4d1f332fb5e01521f4de3199a896d158ddd", size = 6238100, upload-time = "2026-03-09T07:56:17.243Z" }, + { url = "https://files.pythonhosted.org/packages/76/1d/edccf27adedb754db7c4511d5eac8b83f004ae948fe2d3509e8b78097d4c/numpy-2.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:77e76d932c49a75617c6d13464e41203cd410956614d0a0e999b25e9e8d27eec", size = 12609816, upload-time = "2026-03-09T07:56:19.089Z" }, + { url = "https://files.pythonhosted.org/packages/92/82/190b99153480076c8dce85f4cfe7d53ea84444145ffa54cb58dcd460d66b/numpy-2.4.3-cp311-cp311-win_arm64.whl", hash = "sha256:eb610595dd91560905c132c709412b512135a60f1851ccbd2c959e136431ff67", size = 10485757, upload-time = "2026-03-09T07:56:21.753Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ed/6388632536f9788cea23a3a1b629f25b43eaacd7d7377e5d6bc7b9deb69b/numpy-2.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61b0cbabbb6126c8df63b9a3a0c4b1f44ebca5e12ff6997b80fcf267fb3150ef", size = 16669628, upload-time = "2026-03-09T07:56:24.252Z" }, + { url = "https://files.pythonhosted.org/packages/74/1b/ee2abfc68e1ce728b2958b6ba831d65c62e1b13ce3017c13943f8f9b5b2e/numpy-2.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7395e69ff32526710748f92cd8c9849b361830968ea3e24a676f272653e8983e", size = 14696872, upload-time = "2026-03-09T07:56:26.991Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d1/780400e915ff5638166f11ca9dc2c5815189f3d7cf6f8759a1685e586413/numpy-2.4.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:abdce0f71dcb4a00e4e77f3faf05e4616ceccfe72ccaa07f47ee79cda3b7b0f4", size = 5203489, upload-time = "2026-03-09T07:56:29.414Z" }, + { url = "https://files.pythonhosted.org/packages/0b/bb/baffa907e9da4cc34a6e556d6d90e032f6d7a75ea47968ea92b4858826c4/numpy-2.4.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:48da3a4ee1336454b07497ff7ec83903efa5505792c4e6d9bf83d99dc07a1e18", size = 6550814, upload-time = "2026-03-09T07:56:32.225Z" }, + { url = "https://files.pythonhosted.org/packages/7b/12/8c9f0c6c95f76aeb20fc4a699c33e9f827fa0d0f857747c73bb7b17af945/numpy-2.4.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32e3bef222ad6b052280311d1d60db8e259e4947052c3ae7dd6817451fc8a4c5", size = 15666601, upload-time = "2026-03-09T07:56:34.461Z" }, + { url = "https://files.pythonhosted.org/packages/bd/79/cc665495e4d57d0aa6fbcc0aa57aa82671dfc78fbf95fe733ed86d98f52a/numpy-2.4.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7dd01a46700b1967487141a66ac1a3cf0dd8ebf1f08db37d46389401512ca97", size = 16621358, upload-time = "2026-03-09T07:56:36.852Z" }, + { url = "https://files.pythonhosted.org/packages/a8/40/b4ecb7224af1065c3539f5ecfff879d090de09608ad1008f02c05c770cb3/numpy-2.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:76f0f283506c28b12bba319c0fab98217e9f9b54e6160e9c79e9f7348ba32e9c", size = 17016135, upload-time = "2026-03-09T07:56:39.337Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b1/6a88e888052eed951afed7a142dcdf3b149a030ca59b4c71eef085858e43/numpy-2.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737f630a337364665aba3b5a77e56a68cc42d350edd010c345d65a3efa3addcc", size = 18345816, upload-time = "2026-03-09T07:56:42.31Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8f/103a60c5f8c3d7fc678c19cd7b2476110da689ccb80bc18050efbaeae183/numpy-2.4.3-cp312-cp312-win32.whl", hash = "sha256:26952e18d82a1dbbc2f008d402021baa8d6fc8e84347a2072a25e08b46d698b9", size = 5960132, upload-time = "2026-03-09T07:56:44.851Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7c/f5ee1bf6ed888494978046a809df2882aad35d414b622893322df7286879/numpy-2.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:65f3c2455188f09678355f5cae1f959a06b778bc66d535da07bf2ef20cd319d5", size = 12316144, upload-time = "2026-03-09T07:56:47.057Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/8d1cb3f7a00f2fb6394140e7e6623696e54c6318a9d9691bb4904672cf42/numpy-2.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:2abad5c7fef172b3377502bde47892439bae394a71bc329f31df0fd829b41a9e", size = 10220364, upload-time = "2026-03-09T07:56:49.849Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d0/1fe47a98ce0df229238b77611340aff92d52691bcbc10583303181abf7fc/numpy-2.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b346845443716c8e542d54112966383b448f4a3ba5c66409771b8c0889485dd3", size = 16665297, upload-time = "2026-03-09T07:56:52.296Z" }, + { url = "https://files.pythonhosted.org/packages/27/d9/4e7c3f0e68dfa91f21c6fb6cf839bc829ec920688b1ce7ec722b1a6202fb/numpy-2.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2629289168f4897a3c4e23dc98d6f1731f0fc0fe52fb9db19f974041e4cc12b9", size = 14691853, upload-time = "2026-03-09T07:56:54.992Z" }, + { url = "https://files.pythonhosted.org/packages/3a/66/bd096b13a87549683812b53ab211e6d413497f84e794fb3c39191948da97/numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee", size = 5198435, upload-time = "2026-03-09T07:56:57.184Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/687722910b5a5601de2135c891108f51dfc873d8e43c8ed9f4ebb440b4a2/numpy-2.4.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:7f3408ff897f8ab07a07fbe2823d7aee6ff644c097cc1f90382511fe982f647f", size = 6546347, upload-time = "2026-03-09T07:56:59.531Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ec/7971c4e98d86c564750393fab8d7d83d0a9432a9d78bb8a163a6dc59967a/numpy-2.4.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:decb0eb8a53c3b009b0962378065589685d66b23467ef5dac16cbe818afde27f", size = 15664626, upload-time = "2026-03-09T07:57:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/7daecbea84ec935b7fc732e18f532073064a3816f0932a40a17f3349185f/numpy-2.4.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5f51900414fc9204a0e0da158ba2ac52b75656e7dce7e77fb9f84bfa343b4cc", size = 16608916, upload-time = "2026-03-09T07:57:04.008Z" }, + { url = "https://files.pythonhosted.org/packages/df/58/2a2b4a817ffd7472dca4421d9f0776898b364154e30c95f42195041dc03b/numpy-2.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6bd06731541f89cdc01b261ba2c9e037f1543df7472517836b78dfb15bd6e476", size = 17015824, upload-time = "2026-03-09T07:57:06.347Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ca/627a828d44e78a418c55f82dd4caea8ea4a8ef24e5144d9e71016e52fb40/numpy-2.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22654fe6be0e5206f553a9250762c653d3698e46686eee53b399ab90da59bd92", size = 18334581, upload-time = "2026-03-09T07:57:09.114Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c0/76f93962fc79955fcba30a429b62304332345f22d4daec1cb33653425643/numpy-2.4.3-cp313-cp313-win32.whl", hash = "sha256:d71e379452a2f670ccb689ec801b1218cd3983e253105d6e83780967e899d687", size = 5958618, upload-time = "2026-03-09T07:57:11.432Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3c/88af0040119209b9b5cb59485fa48b76f372c73068dbf9254784b975ac53/numpy-2.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:0a60e17a14d640f49146cb38e3f105f571318db7826d9b6fef7e4dce758faecd", size = 12312824, upload-time = "2026-03-09T07:57:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/58/ce/3d07743aced3d173f877c3ef6a454c2174ba42b584ab0b7e6d99374f51ed/numpy-2.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:c9619741e9da2059cd9c3f206110b97583c7152c1dc9f8aafd4beb450ac1c89d", size = 10221218, upload-time = "2026-03-09T07:57:16.183Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/d96b02a91d09e9d97862f4fc8bfebf5400f567d8eb1fe4b0cc4795679c15/numpy-2.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7aa4e54f6469300ebca1d9eb80acd5253cdfa36f2c03d79a35883687da430875", size = 14819570, upload-time = "2026-03-09T07:57:18.564Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ca/0b1aba3905fdfa3373d523b2b15b19029f4f3031c87f4066bd9d20ef6c6b/numpy-2.4.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d1b90d840b25874cf5cd20c219af10bac3667db3876d9a495609273ebe679070", size = 5326113, upload-time = "2026-03-09T07:57:21.052Z" }, + { url = "https://files.pythonhosted.org/packages/c0/63/406e0fd32fcaeb94180fd6a4c41e55736d676c54346b7efbce548b94a914/numpy-2.4.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a749547700de0a20a6718293396ec237bb38218049cfce788e08fcb716e8cf73", size = 6646370, upload-time = "2026-03-09T07:57:22.804Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d0/10f7dc157d4b37af92720a196be6f54f889e90dcd30dce9dc657ed92c257/numpy-2.4.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f3c4a151a2e529adf49c1d54f0f57ff8f9b233ee4d44af623a81553ab86368", size = 15723499, upload-time = "2026-03-09T07:57:24.693Z" }, + { url = "https://files.pythonhosted.org/packages/66/f1/d1c2bf1161396629701bc284d958dc1efa3a5a542aab83cf11ee6eb4cba5/numpy-2.4.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22c31dc07025123aedf7f2db9e91783df13f1776dc52c6b22c620870dc0fab22", size = 16657164, upload-time = "2026-03-09T07:57:27.676Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/cca19230b740af199ac47331a21c71e7a3d0ba59661350483c1600d28c37/numpy-2.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:148d59127ac95979d6f07e4d460f934ebdd6eed641db9c0db6c73026f2b2101a", size = 17081544, upload-time = "2026-03-09T07:57:30.664Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c5/9602b0cbb703a0936fb40f8a95407e8171935b15846de2f0776e08af04c7/numpy-2.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a97cbf7e905c435865c2d939af3d93f99d18eaaa3cabe4256f4304fb51604349", size = 18380290, upload-time = "2026-03-09T07:57:33.763Z" }, + { url = "https://files.pythonhosted.org/packages/ed/81/9f24708953cd30be9ee36ec4778f4b112b45165812f2ada4cc5ea1c1f254/numpy-2.4.3-cp313-cp313t-win32.whl", hash = "sha256:be3b8487d725a77acccc9924f65fd8bce9af7fac8c9820df1049424a2115af6c", size = 6082814, upload-time = "2026-03-09T07:57:36.491Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9e/52f6eaa13e1a799f0ab79066c17f7016a4a8ae0c1aefa58c82b4dab690b4/numpy-2.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1ec84fd7c8e652b0f4aaaf2e6e9cc8eaa9b1b80a537e06b2e3a2fb176eedcb26", size = 12452673, upload-time = "2026-03-09T07:57:38.281Z" }, + { url = "https://files.pythonhosted.org/packages/c4/04/b8cece6ead0b30c9fbd99bb835ad7ea0112ac5f39f069788c5558e3b1ab2/numpy-2.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:120df8c0a81ebbf5b9020c91439fccd85f5e018a927a39f624845be194a2be02", size = 10290907, upload-time = "2026-03-09T07:57:40.747Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/3936f79adebf8caf81bd7a599b90a561334a658be4dcc7b6329ebf4ee8de/numpy-2.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5884ce5c7acfae1e4e1b6fde43797d10aa506074d25b531b4f54bde33c0c31d4", size = 16664563, upload-time = "2026-03-09T07:57:43.817Z" }, + { url = "https://files.pythonhosted.org/packages/9b/62/760f2b55866b496bb1fa7da2a6db076bef908110e568b02fcfc1422e2a3a/numpy-2.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:297837823f5bc572c5f9379b0c9f3a3365f08492cbdc33bcc3af174372ebb168", size = 14702161, upload-time = "2026-03-09T07:57:46.169Z" }, + { url = "https://files.pythonhosted.org/packages/32/af/a7a39464e2c0a21526fb4fb76e346fb172ebc92f6d1c7a07c2c139cc17b1/numpy-2.4.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a111698b4a3f8dcbe54c64a7708f049355abd603e619013c346553c1fd4ca90b", size = 5208738, upload-time = "2026-03-09T07:57:48.506Z" }, + { url = "https://files.pythonhosted.org/packages/29/8c/2a0cf86a59558fa078d83805589c2de490f29ed4fb336c14313a161d358a/numpy-2.4.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:4bd4741a6a676770e0e97fe9ab2e51de01183df3dcbcec591d26d331a40de950", size = 6543618, upload-time = "2026-03-09T07:57:50.591Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b8/612ce010c0728b1c363fa4ea3aa4c22fe1c5da1de008486f8c2f5cb92fae/numpy-2.4.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54f29b877279d51e210e0c80709ee14ccbbad647810e8f3d375561c45ef613dd", size = 15680676, upload-time = "2026-03-09T07:57:52.34Z" }, + { url = "https://files.pythonhosted.org/packages/a9/7e/4f120ecc54ba26ddf3dc348eeb9eb063f421de65c05fc961941798feea18/numpy-2.4.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:679f2a834bae9020f81534671c56fd0cc76dd7e5182f57131478e23d0dc59e24", size = 16613492, upload-time = "2026-03-09T07:57:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/2c/86/1b6020db73be330c4b45d5c6ee4295d59cfeef0e3ea323959d053e5a6909/numpy-2.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d84f0f881cb2225c2dfd7f78a10a5645d487a496c6668d6cc39f0f114164f3d0", size = 17031789, upload-time = "2026-03-09T07:57:57.641Z" }, + { url = "https://files.pythonhosted.org/packages/07/3a/3b90463bf41ebc21d1b7e06079f03070334374208c0f9a1f05e4ae8455e7/numpy-2.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d213c7e6e8d211888cc359bab7199670a00f5b82c0978b9d1c75baf1eddbeac0", size = 18339941, upload-time = "2026-03-09T07:58:00.577Z" }, + { url = "https://files.pythonhosted.org/packages/a8/74/6d736c4cd962259fd8bae9be27363eb4883a2f9069763747347544c2a487/numpy-2.4.3-cp314-cp314-win32.whl", hash = "sha256:52077feedeff7c76ed7c9f1a0428558e50825347b7545bbb8523da2cd55c547a", size = 6007503, upload-time = "2026-03-09T07:58:03.331Z" }, + { url = "https://files.pythonhosted.org/packages/48/39/c56ef87af669364356bb011922ef0734fc49dad51964568634c72a009488/numpy-2.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:0448e7f9caefb34b4b7dd2b77f21e8906e5d6f0365ad525f9f4f530b13df2afc", size = 12444915, upload-time = "2026-03-09T07:58:06.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1f/ab8528e38d295fd349310807496fabb7cf9fe2e1f70b97bc20a483ea9d4a/numpy-2.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:b44fd60341c4d9783039598efadd03617fa28d041fc37d22b62d08f2027fa0e7", size = 10494875, upload-time = "2026-03-09T07:58:08.734Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ef/b7c35e4d5ef141b836658ab21a66d1a573e15b335b1d111d31f26c8ef80f/numpy-2.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a195f4216be9305a73c0e91c9b026a35f2161237cf1c6de9b681637772ea657", size = 14822225, upload-time = "2026-03-09T07:58:11.034Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8d/7730fa9278cf6648639946cc816e7cc89f0d891602584697923375f801ed/numpy-2.4.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:cd32fbacb9fd1bf041bf8e89e4576b6f00b895f06d00914820ae06a616bdfef7", size = 5328769, upload-time = "2026-03-09T07:58:13.67Z" }, + { url = "https://files.pythonhosted.org/packages/47/01/d2a137317c958b074d338807c1b6a383406cdf8b8e53b075d804cc3d211d/numpy-2.4.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:2e03c05abaee1f672e9d67bc858f300b5ccba1c21397211e8d77d98350972093", size = 6649461, upload-time = "2026-03-09T07:58:15.912Z" }, + { url = "https://files.pythonhosted.org/packages/5c/34/812ce12bc0f00272a4b0ec0d713cd237cb390666eb6206323d1cc9cedbb2/numpy-2.4.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d1ce23cce91fcea443320a9d0ece9b9305d4368875bab09538f7a5b4131938a", size = 15725809, upload-time = "2026-03-09T07:58:17.787Z" }, + { url = "https://files.pythonhosted.org/packages/25/c0/2aed473a4823e905e765fee3dc2cbf504bd3e68ccb1150fbdabd5c39f527/numpy-2.4.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c59020932feb24ed49ffd03704fbab89f22aa9c0d4b180ff45542fe8918f5611", size = 16655242, upload-time = "2026-03-09T07:58:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c8/7e052b2fc87aa0e86de23f20e2c42bd261c624748aa8efd2c78f7bb8d8c6/numpy-2.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9684823a78a6cd6ad7511fc5e25b07947d1d5b5e2812c93fe99d7d4195130720", size = 17080660, upload-time = "2026-03-09T07:58:23.067Z" }, + { url = "https://files.pythonhosted.org/packages/f3/3d/0876746044db2adcb11549f214d104f2e1be00f07a67edbb4e2812094847/numpy-2.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0200b25c687033316fb39f0ff4e3e690e8957a2c3c8d22499891ec58c37a3eb5", size = 18380384, upload-time = "2026-03-09T07:58:25.839Z" }, + { url = "https://files.pythonhosted.org/packages/07/12/8160bea39da3335737b10308df4f484235fd297f556745f13092aa039d3b/numpy-2.4.3-cp314-cp314t-win32.whl", hash = "sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0", size = 6154547, upload-time = "2026-03-09T07:58:28.289Z" }, + { url = "https://files.pythonhosted.org/packages/42/f3/76534f61f80d74cc9cdf2e570d3d4eeb92c2280a27c39b0aaf471eda7b48/numpy-2.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b", size = 12633645, upload-time = "2026-03-09T07:58:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/1f/b6/7c0d4334c15983cec7f92a69e8ce9b1e6f31857e5ee3a413ac424e6bd63d/numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e", size = 10565454, upload-time = "2026-03-09T07:58:33.031Z" }, + { url = "https://files.pythonhosted.org/packages/64/e4/4dab9fb43c83719c29241c535d9e07be73bea4bc0c6686c5816d8e1b6689/numpy-2.4.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c6b124bfcafb9e8d3ed09130dbee44848c20b3e758b6bbf006e641778927c028", size = 16834892, upload-time = "2026-03-09T07:58:35.334Z" }, + { url = "https://files.pythonhosted.org/packages/c9/29/f8b6d4af90fed3dfda84ebc0df06c9833d38880c79ce954e5b661758aa31/numpy-2.4.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:76dbb9d4e43c16cf9aa711fcd8de1e2eeb27539dcefb60a1d5e9f12fae1d1ed8", size = 14893070, upload-time = "2026-03-09T07:58:37.7Z" }, + { url = "https://files.pythonhosted.org/packages/9a/04/a19b3c91dbec0a49269407f15d5753673a09832daed40c45e8150e6fa558/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:29363fbfa6f8ee855d7569c96ce524845e3d726d6c19b29eceec7dd555dab152", size = 5399609, upload-time = "2026-03-09T07:58:39.853Z" }, + { url = "https://files.pythonhosted.org/packages/79/34/4d73603f5420eab89ea8a67097b31364bf7c30f811d4dd84b1659c7476d9/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:bc71942c789ef415a37f0d4eab90341425a00d538cd0642445d30b41023d3395", size = 6714355, upload-time = "2026-03-09T07:58:42.365Z" }, + { url = "https://files.pythonhosted.org/packages/58/ad/1100d7229bb248394939a12a8074d485b655e8ed44207d328fdd7fcebc7b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e58765ad74dcebd3ef0208a5078fba32dc8ec3578fe84a604432950cd043d79", size = 15800434, upload-time = "2026-03-09T07:58:44.837Z" }, + { url = "https://files.pythonhosted.org/packages/0c/fd/16d710c085d28ba4feaf29ac60c936c9d662e390344f94a6beaa2ac9899b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e236dbda4e1d319d681afcbb136c0c4a8e0f1a5c58ceec2adebb547357fe857", size = 16729409, upload-time = "2026-03-09T07:58:47.972Z" }, + { url = "https://files.pythonhosted.org/packages/57/a7/b35835e278c18b85206834b3aa3abe68e77a98769c59233d1f6300284781/numpy-2.4.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4b42639cdde6d24e732ff823a3fa5b701d8acad89c4142bc1d0bd6dc85200ba5", size = 12504685, upload-time = "2026-03-09T07:58:50.525Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/1d/4049a9e8698361cc1a1aa03a6c59e4fa4c71e0c0f94a30f988a6876a2ae6/opentelemetry_api-1.40.0.tar.gz", hash = "sha256:159be641c0b04d11e9ecd576906462773eb97ae1b657730f0ecf64d32071569f", size = 70851, upload-time = "2026-03-04T14:17:21.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/bf/93795954016c522008da367da292adceed71cca6ee1717e1d64c83089099/opentelemetry_api-1.40.0-py3-none-any.whl", hash = "sha256:82dd69331ae74b06f6a874704be0cfaa49a1650e1537d4a813b86ecef7d0ecf9", size = 68676, upload-time = "2026-03-04T14:17:01.24Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pandas" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/0c/b28ed414f080ee0ad153f848586d61d1878f91689950f037f976ce15f6c8/pandas-3.0.1.tar.gz", hash = "sha256:4186a699674af418f655dbd420ed87f50d56b4cd6603784279d9eef6627823c8", size = 4641901, upload-time = "2026-02-17T22:20:16.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/07/c7087e003ceee9b9a82539b40414ec557aa795b584a1a346e89180853d79/pandas-3.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de09668c1bf3b925c07e5762291602f0d789eca1b3a781f99c1c78f6cac0e7ea", size = 10323380, upload-time = "2026-02-17T22:18:16.133Z" }, + { url = "https://files.pythonhosted.org/packages/c1/27/90683c7122febeefe84a56f2cde86a9f05f68d53885cebcc473298dfc33e/pandas-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:24ba315ba3d6e5806063ac6eb717504e499ce30bd8c236d8693a5fd3f084c796", size = 9923455, upload-time = "2026-02-17T22:18:19.13Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f1/ed17d927f9950643bc7631aa4c99ff0cc83a37864470bc419345b656a41f/pandas-3.0.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:406ce835c55bac912f2a0dcfaf27c06d73c6b04a5dde45f1fd3169ce31337389", size = 10753464, upload-time = "2026-02-17T22:18:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/2e/7c/870c7e7daec2a6c7ff2ac9e33b23317230d4e4e954b35112759ea4a924a7/pandas-3.0.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:830994d7e1f31dd7e790045235605ab61cff6c94defc774547e8b7fdfbff3dc7", size = 11255234, upload-time = "2026-02-17T22:18:24.175Z" }, + { url = "https://files.pythonhosted.org/packages/5c/39/3653fe59af68606282b989c23d1a543ceba6e8099cbcc5f1d506a7bae2aa/pandas-3.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a64ce8b0f2de1d2efd2ae40b0abe7f8ae6b29fbfb3812098ed5a6f8e235ad9bf", size = 11767299, upload-time = "2026-02-17T22:18:26.824Z" }, + { url = "https://files.pythonhosted.org/packages/9b/31/1daf3c0c94a849c7a8dab8a69697b36d313b229918002ba3e409265c7888/pandas-3.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9832c2c69da24b602c32e0c7b1b508a03949c18ba08d4d9f1c1033426685b447", size = 12333292, upload-time = "2026-02-17T22:18:28.996Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/af63f83cd6ca603a00fe8530c10a60f0879265b8be00b5930e8e78c5b30b/pandas-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:84f0904a69e7365f79a0c77d3cdfccbfb05bf87847e3a51a41e1426b0edb9c79", size = 9892176, upload-time = "2026-02-17T22:18:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/79/ab/9c776b14ac4b7b4140788eca18468ea39894bc7340a408f1d1e379856a6b/pandas-3.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:4a68773d5a778afb31d12e34f7dd4612ab90de8c6fb1d8ffe5d4a03b955082a1", size = 9151328, upload-time = "2026-02-17T22:18:35.721Z" }, + { url = "https://files.pythonhosted.org/packages/37/51/b467209c08dae2c624873d7491ea47d2b47336e5403309d433ea79c38571/pandas-3.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:476f84f8c20c9f5bc47252b66b4bb25e1a9fc2fa98cead96744d8116cb85771d", size = 10344357, upload-time = "2026-02-17T22:18:38.262Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f1/e2567ffc8951ab371db2e40b2fe068e36b81d8cf3260f06ae508700e5504/pandas-3.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0ab749dfba921edf641d4036c4c21c0b3ea70fea478165cb98a998fb2a261955", size = 9884543, upload-time = "2026-02-17T22:18:41.476Z" }, + { url = "https://files.pythonhosted.org/packages/d7/39/327802e0b6d693182403c144edacbc27eb82907b57062f23ef5a4c4a5ea7/pandas-3.0.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8e36891080b87823aff3640c78649b91b8ff6eea3c0d70aeabd72ea43ab069b", size = 10396030, upload-time = "2026-02-17T22:18:43.822Z" }, + { url = "https://files.pythonhosted.org/packages/3d/fe/89d77e424365280b79d99b3e1e7d606f5165af2f2ecfaf0c6d24c799d607/pandas-3.0.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:532527a701281b9dd371e2f582ed9094f4c12dd9ffb82c0c54ee28d8ac9520c4", size = 10876435, upload-time = "2026-02-17T22:18:45.954Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a6/2a75320849dd154a793f69c951db759aedb8d1dd3939eeacda9bdcfa1629/pandas-3.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:356e5c055ed9b0da1580d465657bc7d00635af4fd47f30afb23025352ba764d1", size = 11405133, upload-time = "2026-02-17T22:18:48.533Z" }, + { url = "https://files.pythonhosted.org/packages/58/53/1d68fafb2e02d7881df66aa53be4cd748d25cbe311f3b3c85c93ea5d30ca/pandas-3.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9d810036895f9ad6345b8f2a338dd6998a74e8483847403582cab67745bff821", size = 11932065, upload-time = "2026-02-17T22:18:50.837Z" }, + { url = "https://files.pythonhosted.org/packages/75/08/67cc404b3a966b6df27b38370ddd96b3b023030b572283d035181854aac5/pandas-3.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:536232a5fe26dd989bd633e7a0c450705fdc86a207fec7254a55e9a22950fe43", size = 9741627, upload-time = "2026-02-17T22:18:53.905Z" }, + { url = "https://files.pythonhosted.org/packages/86/4f/caf9952948fb00d23795f09b893d11f1cacb384e666854d87249530f7cbe/pandas-3.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f463ebfd8de7f326d38037c7363c6dacb857c5881ab8961fb387804d6daf2f7", size = 9052483, upload-time = "2026-02-17T22:18:57.31Z" }, + { url = "https://files.pythonhosted.org/packages/0b/48/aad6ec4f8d007534c091e9a7172b3ec1b1ee6d99a9cbb936b5eab6c6cf58/pandas-3.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5272627187b5d9c20e55d27caf5f2cd23e286aba25cadf73c8590e432e2b7262", size = 10317509, upload-time = "2026-02-17T22:18:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/a8/14/5990826f779f79148ae9d3a2c39593dc04d61d5d90541e71b5749f35af95/pandas-3.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:661e0f665932af88c7877f31da0dc743fe9c8f2524bdffe23d24fdcb67ef9d56", size = 9860561, upload-time = "2026-02-17T22:19:02.265Z" }, + { url = "https://files.pythonhosted.org/packages/fa/80/f01ff54664b6d70fed71475543d108a9b7c888e923ad210795bef04ffb7d/pandas-3.0.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75e6e292ff898679e47a2199172593d9f6107fd2dd3617c22c2946e97d5df46e", size = 10365506, upload-time = "2026-02-17T22:19:05.017Z" }, + { url = "https://files.pythonhosted.org/packages/f2/85/ab6d04733a7d6ff32bfc8382bf1b07078228f5d6ebec5266b91bfc5c4ff7/pandas-3.0.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ff8cf1d2896e34343197685f432450ec99a85ba8d90cce2030c5eee2ef98791", size = 10873196, upload-time = "2026-02-17T22:19:07.204Z" }, + { url = "https://files.pythonhosted.org/packages/48/a9/9301c83d0b47c23ac5deab91c6b39fd98d5b5db4d93b25df8d381451828f/pandas-3.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eca8b4510f6763f3d37359c2105df03a7a221a508f30e396a51d0713d462e68a", size = 11370859, upload-time = "2026-02-17T22:19:09.436Z" }, + { url = "https://files.pythonhosted.org/packages/59/fe/0c1fc5bd2d29c7db2ab372330063ad555fb83e08422829c785f5ec2176ca/pandas-3.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:06aff2ad6f0b94a17822cf8b83bbb563b090ed82ff4fe7712db2ce57cd50d9b8", size = 11924584, upload-time = "2026-02-17T22:19:11.562Z" }, + { url = "https://files.pythonhosted.org/packages/d6/7d/216a1588b65a7aa5f4535570418a599d943c85afb1d95b0876fc00aa1468/pandas-3.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:9fea306c783e28884c29057a1d9baa11a349bbf99538ec1da44c8476563d1b25", size = 9742769, upload-time = "2026-02-17T22:19:13.926Z" }, + { url = "https://files.pythonhosted.org/packages/c4/cb/810a22a6af9a4e97c8ab1c946b47f3489c5bca5adc483ce0ffc84c9cc768/pandas-3.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:a8d37a43c52917427e897cb2e429f67a449327394396a81034a4449b99afda59", size = 9043855, upload-time = "2026-02-17T22:19:16.09Z" }, + { url = "https://files.pythonhosted.org/packages/92/fa/423c89086cca1f039cf1253c3ff5b90f157b5b3757314aa635f6bf3e30aa/pandas-3.0.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d54855f04f8246ed7b6fc96b05d4871591143c46c0b6f4af874764ed0d2d6f06", size = 10752673, upload-time = "2026-02-17T22:19:18.304Z" }, + { url = "https://files.pythonhosted.org/packages/22/23/b5a08ec1f40020397f0faba72f1e2c11f7596a6169c7b3e800abff0e433f/pandas-3.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e1b677accee34a09e0dc2ce5624e4a58a1870ffe56fc021e9caf7f23cd7668f", size = 10404967, upload-time = "2026-02-17T22:19:20.726Z" }, + { url = "https://files.pythonhosted.org/packages/5c/81/94841f1bb4afdc2b52a99daa895ac2c61600bb72e26525ecc9543d453ebc/pandas-3.0.1-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9cabbdcd03f1b6cd254d6dda8ae09b0252524be1592594c00b7895916cb1324", size = 10320575, upload-time = "2026-02-17T22:19:24.919Z" }, + { url = "https://files.pythonhosted.org/packages/0a/8b/2ae37d66a5342a83adadfd0cb0b4bf9c3c7925424dd5f40d15d6cfaa35ee/pandas-3.0.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ae2ab1f166668b41e770650101e7090824fd34d17915dd9cd479f5c5e0065e9", size = 10710921, upload-time = "2026-02-17T22:19:27.181Z" }, + { url = "https://files.pythonhosted.org/packages/a2/61/772b2e2757855e232b7ccf7cb8079a5711becb3a97f291c953def15a833f/pandas-3.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6bf0603c2e30e2cafac32807b06435f28741135cb8697eae8b28c7d492fc7d76", size = 11334191, upload-time = "2026-02-17T22:19:29.411Z" }, + { url = "https://files.pythonhosted.org/packages/1b/08/b16c6df3ef555d8495d1d265a7963b65be166785d28f06a350913a4fac78/pandas-3.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c426422973973cae1f4a23e51d4ae85974f44871b24844e4f7de752dd877098", size = 11782256, upload-time = "2026-02-17T22:19:32.34Z" }, + { url = "https://files.pythonhosted.org/packages/55/80/178af0594890dee17e239fca96d3d8670ba0f5ff59b7d0439850924a9c09/pandas-3.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b03f91ae8c10a85c1613102c7bef5229b5379f343030a3ccefeca8a33414cf35", size = 10485047, upload-time = "2026-02-17T22:19:34.605Z" }, + { url = "https://files.pythonhosted.org/packages/bb/8b/4bb774a998b97e6c2fd62a9e6cfdaae133b636fd1c468f92afb4ae9a447a/pandas-3.0.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:99d0f92ed92d3083d140bf6b97774f9f13863924cf3f52a70711f4e7588f9d0a", size = 10322465, upload-time = "2026-02-17T22:19:36.803Z" }, + { url = "https://files.pythonhosted.org/packages/72/3a/5b39b51c64159f470f1ca3b1c2a87da290657ca022f7cd11442606f607d1/pandas-3.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3b66857e983208654294bb6477b8a63dee26b37bdd0eb34d010556e91261784f", size = 9910632, upload-time = "2026-02-17T22:19:39.001Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f7/b449ffb3f68c11da12fc06fbf6d2fa3a41c41e17d0284d23a79e1c13a7e4/pandas-3.0.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56cf59638bf24dc9bdf2154c81e248b3289f9a09a6d04e63608c159022352749", size = 10440535, upload-time = "2026-02-17T22:19:41.157Z" }, + { url = "https://files.pythonhosted.org/packages/55/77/6ea82043db22cb0f2bbfe7198da3544000ddaadb12d26be36e19b03a2dc5/pandas-3.0.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1a9f55e0f46951874b863d1f3906dcb57df2d9be5c5847ba4dfb55b2c815249", size = 10893940, upload-time = "2026-02-17T22:19:43.493Z" }, + { url = "https://files.pythonhosted.org/packages/03/30/f1b502a72468c89412c1b882a08f6eed8a4ee9dc033f35f65d0663df6081/pandas-3.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1849f0bba9c8a2fb0f691d492b834cc8dadf617e29015c66e989448d58d011ee", size = 11442711, upload-time = "2026-02-17T22:19:46.074Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f0/ebb6ddd8fc049e98cabac5c2924d14d1dda26a20adb70d41ea2e428d3ec4/pandas-3.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3d288439e11b5325b02ae6e9cc83e6805a62c40c5a6220bea9beb899c073b1c", size = 11963918, upload-time = "2026-02-17T22:19:48.838Z" }, + { url = "https://files.pythonhosted.org/packages/09/f8/8ce132104074f977f907442790eaae24e27bce3b3b454e82faa3237ff098/pandas-3.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:93325b0fe372d192965f4cca88d97667f49557398bbf94abdda3bf1b591dbe66", size = 9862099, upload-time = "2026-02-17T22:19:51.081Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b7/6af9aac41ef2456b768ef0ae60acf8abcebb450a52043d030a65b4b7c9bd/pandas-3.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:97ca08674e3287c7148f4858b01136f8bdfe7202ad25ad04fec602dd1d29d132", size = 9185333, upload-time = "2026-02-17T22:19:53.266Z" }, + { url = "https://files.pythonhosted.org/packages/66/fc/848bb6710bc6061cb0c5badd65b92ff75c81302e0e31e496d00029fe4953/pandas-3.0.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:58eeb1b2e0fb322befcf2bbc9ba0af41e616abadb3d3414a6bc7167f6cbfce32", size = 10772664, upload-time = "2026-02-17T22:19:55.806Z" }, + { url = "https://files.pythonhosted.org/packages/69/5c/866a9bbd0f79263b4b0db6ec1a341be13a1473323f05c122388e0f15b21d/pandas-3.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cd9af1276b5ca9e298bd79a26bda32fa9cc87ed095b2a9a60978d2ca058eaf87", size = 10421286, upload-time = "2026-02-17T22:19:58.091Z" }, + { url = "https://files.pythonhosted.org/packages/51/a4/2058fb84fb1cfbfb2d4a6d485e1940bb4ad5716e539d779852494479c580/pandas-3.0.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f87a04984d6b63788327cd9f79dda62b7f9043909d2440ceccf709249ca988", size = 10342050, upload-time = "2026-02-17T22:20:01.376Z" }, + { url = "https://files.pythonhosted.org/packages/22/1b/674e89996cc4be74db3c4eb09240c4bb549865c9c3f5d9b086ff8fcfbf00/pandas-3.0.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85fe4c4df62e1e20f9db6ebfb88c844b092c22cd5324bdcf94bfa2fc1b391221", size = 10740055, upload-time = "2026-02-17T22:20:04.328Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f8/e954b750764298c22fa4614376531fe63c521ef517e7059a51f062b87dca/pandas-3.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:331ca75a2f8672c365ae25c0b29e46f5ac0c6551fdace8eec4cd65e4fac271ff", size = 11357632, upload-time = "2026-02-17T22:20:06.647Z" }, + { url = "https://files.pythonhosted.org/packages/6d/02/c6e04b694ffd68568297abd03588b6d30295265176a5c01b7459d3bc35a3/pandas-3.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:15860b1fdb1973fffade772fdb931ccf9b2f400a3f5665aef94a00445d7d8dd5", size = 11810974, upload-time = "2026-02-17T22:20:08.946Z" }, + { url = "https://files.pythonhosted.org/packages/89/41/d7dfb63d2407f12055215070c42fc6ac41b66e90a2946cdc5e759058398b/pandas-3.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:44f1364411d5670efa692b146c748f4ed013df91ee91e9bec5677fb1fd58b937", size = 10884622, upload-time = "2026-02-17T22:20:11.711Z" }, + { url = "https://files.pythonhosted.org/packages/68/b0/34937815889fa982613775e4b97fddd13250f11012d769949c5465af2150/pandas-3.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:108dd1790337a494aa80e38def654ca3f0968cf4f362c85f44c15e471667102d", size = 9452085, upload-time = "2026-02-17T22:20:14.331Z" }, +] + +[[package]] +name = "parso" +version = "0.8.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/76/a1e769043c0c0c9fe391b702539d594731a4362334cdf4dc25d0c09761e7/parso-0.8.6.tar.gz", hash = "sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd", size = 401621, upload-time = "2026-02-09T15:45:24.425Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" }, +] + +[[package]] +name = "partd" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "locket" }, + { name = "toolz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/3a/3f06f34820a31257ddcabdfafc2672c5816be79c7e353b02c1f318daa7d4/partd-1.4.2.tar.gz", hash = "sha256:d022c33afbdc8405c226621b015e8067888173d85f7f5ecebb3cafed9a20f02c", size = 21029, upload-time = "2024-05-06T19:51:41.945Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl", hash = "sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f", size = 18905, upload-time = "2024-05-06T19:51:39.271Z" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + +[[package]] +name = "pillow" +version = "12.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084, upload-time = "2026-02-11T04:20:27.501Z" }, + { url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866, upload-time = "2026-02-11T04:20:29.827Z" }, + { url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148, upload-time = "2026-02-11T04:20:31.329Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007, upload-time = "2026-02-11T04:20:34.225Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418, upload-time = "2026-02-11T04:20:35.858Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590, upload-time = "2026-02-11T04:20:37.91Z" }, + { url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655, upload-time = "2026-02-11T04:20:39.496Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286, upload-time = "2026-02-11T04:20:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663, upload-time = "2026-02-11T04:20:43.184Z" }, + { url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448, upload-time = "2026-02-11T04:20:44.696Z" }, + { url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651, upload-time = "2026-02-11T04:20:46.243Z" }, + { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" }, + { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" }, + { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" }, + { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" }, + { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, + { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" }, + { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" }, + { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" }, + { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" }, + { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" }, + { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" }, + { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" }, + { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" }, + { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" }, + { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" }, + { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" }, + { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" }, + { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" }, + { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" }, + { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" }, + { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" }, + { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" }, + { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" }, + { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" }, + { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" }, + { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606, upload-time = "2026-02-11T04:22:52.106Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321, upload-time = "2026-02-11T04:22:53.827Z" }, + { url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579, upload-time = "2026-02-11T04:22:56.094Z" }, + { url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094, upload-time = "2026-02-11T04:22:58.288Z" }, + { url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850, upload-time = "2026-02-11T04:23:00.554Z" }, + { url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343, upload-time = "2026-02-11T04:23:02.934Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, +] + +[[package]] +name = "pyarrow" +version = "23.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/22/134986a4cc224d593c1afde5494d18ff629393d74cc2eddb176669f234a4/pyarrow-23.0.1.tar.gz", hash = "sha256:b8c5873e33440b2bc2f4a79d2b47017a89c5a24116c055625e6f2ee50523f019", size = 1167336, upload-time = "2026-02-16T10:14:12.39Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/41/8e6b6ef7e225d4ceead8459427a52afdc23379768f54dd3566014d7618c1/pyarrow-23.0.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:6f0147ee9e0386f519c952cc670eb4a8b05caa594eeffe01af0e25f699e4e9bb", size = 34302230, upload-time = "2026-02-16T10:09:03.859Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4a/1472c00392f521fea03ae93408bf445cc7bfa1ab81683faf9bc188e36629/pyarrow-23.0.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:0ae6e17c828455b6265d590100c295193f93cc5675eb0af59e49dbd00d2de350", size = 35850050, upload-time = "2026-02-16T10:09:11.877Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b2/bd1f2f05ded56af7f54d702c8364c9c43cd6abb91b0e9933f3d77b4f4132/pyarrow-23.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:fed7020203e9ef273360b9e45be52a2a47d3103caf156a30ace5247ffb51bdbd", size = 44491918, upload-time = "2026-02-16T10:09:18.144Z" }, + { url = "https://files.pythonhosted.org/packages/0b/62/96459ef5b67957eac38a90f541d1c28833d1b367f014a482cb63f3b7cd2d/pyarrow-23.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:26d50dee49d741ac0e82185033488d28d35be4d763ae6f321f97d1140eb7a0e9", size = 47562811, upload-time = "2026-02-16T10:09:25.792Z" }, + { url = "https://files.pythonhosted.org/packages/7d/94/1170e235add1f5f45a954e26cd0e906e7e74e23392dcb560de471f7366ec/pyarrow-23.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3c30143b17161310f151f4a2bcfe41b5ff744238c1039338779424e38579d701", size = 48183766, upload-time = "2026-02-16T10:09:34.645Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/39a42af4570377b99774cdb47f63ee6c7da7616bd55b3d5001aa18edfe4f/pyarrow-23.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db2190fa79c80a23fdd29fef4b8992893f024ae7c17d2f5f4db7171fa30c2c78", size = 50607669, upload-time = "2026-02-16T10:09:44.153Z" }, + { url = "https://files.pythonhosted.org/packages/00/ca/db94101c187f3df742133ac837e93b1f269ebdac49427f8310ee40b6a58f/pyarrow-23.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:f00f993a8179e0e1c9713bcc0baf6d6c01326a406a9c23495ec1ba9c9ebf2919", size = 27527698, upload-time = "2026-02-16T10:09:50.263Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4b/4166bb5abbfe6f750fc60ad337c43ecf61340fa52ab386da6e8dbf9e63c4/pyarrow-23.0.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:f4b0dbfa124c0bb161f8b5ebb40f1a680b70279aa0c9901d44a2b5a20806039f", size = 34214575, upload-time = "2026-02-16T10:09:56.225Z" }, + { url = "https://files.pythonhosted.org/packages/e1/da/3f941e3734ac8088ea588b53e860baeddac8323ea40ce22e3d0baa865cc9/pyarrow-23.0.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:7707d2b6673f7de054e2e83d59f9e805939038eebe1763fe811ee8fa5c0cd1a7", size = 35832540, upload-time = "2026-02-16T10:10:03.428Z" }, + { url = "https://files.pythonhosted.org/packages/88/7c/3d841c366620e906d54430817531b877ba646310296df42ef697308c2705/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:86ff03fb9f1a320266e0de855dee4b17da6794c595d207f89bba40d16b5c78b9", size = 44470940, upload-time = "2026-02-16T10:10:10.704Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a5/da83046273d990f256cb79796a190bbf7ec999269705ddc609403f8c6b06/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:813d99f31275919c383aab17f0f455a04f5a429c261cc411b1e9a8f5e4aaaa05", size = 47586063, upload-time = "2026-02-16T10:10:17.95Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/b7d2ebcff47a514f47f9da1e74b7949138c58cfeb108cdd4ee62f43f0cf3/pyarrow-23.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bf5842f960cddd2ef757d486041d57c96483efc295a8c4a0e20e704cbbf39c67", size = 48173045, upload-time = "2026-02-16T10:10:25.363Z" }, + { url = "https://files.pythonhosted.org/packages/43/b2/b40961262213beaba6acfc88698eb773dfce32ecdf34d19291db94c2bd73/pyarrow-23.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564baf97c858ecc03ec01a41062e8f4698abc3e6e2acd79c01c2e97880a19730", size = 50621741, upload-time = "2026-02-16T10:10:33.477Z" }, + { url = "https://files.pythonhosted.org/packages/f6/70/1fdda42d65b28b078e93d75d371b2185a61da89dda4def8ba6ba41ebdeb4/pyarrow-23.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:07deae7783782ac7250989a7b2ecde9b3c343a643f82e8a4df03d93b633006f0", size = 27620678, upload-time = "2026-02-16T10:10:39.31Z" }, + { url = "https://files.pythonhosted.org/packages/47/10/2cbe4c6f0fb83d2de37249567373d64327a5e4d8db72f486db42875b08f6/pyarrow-23.0.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6b8fda694640b00e8af3c824f99f789e836720aa8c9379fb435d4c4953a756b8", size = 34210066, upload-time = "2026-02-16T10:10:45.487Z" }, + { url = "https://files.pythonhosted.org/packages/cb/4f/679fa7e84dadbaca7a65f7cdba8d6c83febbd93ca12fa4adf40ba3b6362b/pyarrow-23.0.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:8ff51b1addc469b9444b7c6f3548e19dc931b172ab234e995a60aea9f6e6025f", size = 35825526, upload-time = "2026-02-16T10:10:52.266Z" }, + { url = "https://files.pythonhosted.org/packages/f9/63/d2747d930882c9d661e9398eefc54f15696547b8983aaaf11d4a2e8b5426/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:71c5be5cbf1e1cb6169d2a0980850bccb558ddc9b747b6206435313c47c37677", size = 44473279, upload-time = "2026-02-16T10:11:01.557Z" }, + { url = "https://files.pythonhosted.org/packages/b3/93/10a48b5e238de6d562a411af6467e71e7aedbc9b87f8d3a35f1560ae30fb/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9b6f4f17b43bc39d56fec96e53fe89d94bac3eb134137964371b45352d40d0c2", size = 47585798, upload-time = "2026-02-16T10:11:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/476943001c54ef078dbf9542280e22741219a184a0632862bca4feccd666/pyarrow-23.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fc13fc6c403d1337acab46a2c4346ca6c9dec5780c3c697cf8abfd5e19b6b37", size = 48179446, upload-time = "2026-02-16T10:11:17.781Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b6/5dd0c47b335fcd8edba9bfab78ad961bd0fd55ebe53468cc393f45e0be60/pyarrow-23.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c16ed4f53247fa3ffb12a14d236de4213a4415d127fe9cebed33d51671113e2", size = 50623972, upload-time = "2026-02-16T10:11:26.185Z" }, + { url = "https://files.pythonhosted.org/packages/d5/09/a532297c9591a727d67760e2e756b83905dd89adb365a7f6e9c72578bcc1/pyarrow-23.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:cecfb12ef629cf6be0b1887f9f86463b0dd3dc3195ae6224e74006be4736035a", size = 27540749, upload-time = "2026-02-16T10:12:23.297Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8e/38749c4b1303e6ae76b3c80618f84861ae0c55dd3c2273842ea6f8258233/pyarrow-23.0.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:29f7f7419a0e30264ea261fdc0e5fe63ce5a6095003db2945d7cd78df391a7e1", size = 34471544, upload-time = "2026-02-16T10:11:32.535Z" }, + { url = "https://files.pythonhosted.org/packages/a3/73/f237b2bc8c669212f842bcfd842b04fc8d936bfc9d471630569132dc920d/pyarrow-23.0.1-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:33d648dc25b51fd8055c19e4261e813dfc4d2427f068bcecc8b53d01b81b0500", size = 35949911, upload-time = "2026-02-16T10:11:39.813Z" }, + { url = "https://files.pythonhosted.org/packages/0c/86/b912195eee0903b5611bf596833def7d146ab2d301afeb4b722c57ffc966/pyarrow-23.0.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd395abf8f91c673dd3589cadc8cc1ee4e8674fa61b2e923c8dd215d9c7d1f41", size = 44520337, upload-time = "2026-02-16T10:11:47.764Z" }, + { url = "https://files.pythonhosted.org/packages/69/c2/f2a717fb824f62d0be952ea724b4f6f9372a17eed6f704b5c9526f12f2f1/pyarrow-23.0.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:00be9576d970c31defb5c32eb72ef585bf600ef6d0a82d5eccaae96639cf9d07", size = 47548944, upload-time = "2026-02-16T10:11:56.607Z" }, + { url = "https://files.pythonhosted.org/packages/84/a7/90007d476b9f0dc308e3bc57b832d004f848fd6c0da601375d20d92d1519/pyarrow-23.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c2139549494445609f35a5cda4eb94e2c9e4d704ce60a095b342f82460c73a83", size = 48236269, upload-time = "2026-02-16T10:12:04.47Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3f/b16fab3e77709856eb6ac328ce35f57a6d4a18462c7ca5186ef31b45e0e0/pyarrow-23.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7044b442f184d84e2351e5084600f0d7343d6117aabcbc1ac78eb1ae11eb4125", size = 50604794, upload-time = "2026-02-16T10:12:11.797Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a1/22df0620a9fac31d68397a75465c344e83c3dfe521f7612aea33e27ab6c0/pyarrow-23.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a35581e856a2fafa12f3f54fce4331862b1cfb0bef5758347a858a4aa9d6bae8", size = 27660642, upload-time = "2026-02-16T10:12:17.746Z" }, + { url = "https://files.pythonhosted.org/packages/8d/1b/6da9a89583ce7b23ac611f183ae4843cd3a6cf54f079549b0e8c14031e73/pyarrow-23.0.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:5df1161da23636a70838099d4aaa65142777185cc0cdba4037a18cee7d8db9ca", size = 34238755, upload-time = "2026-02-16T10:12:32.819Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b5/d58a241fbe324dbaeb8df07be6af8752c846192d78d2272e551098f74e88/pyarrow-23.0.1-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:fa8e51cb04b9f8c9c5ace6bab63af9a1f88d35c0d6cbf53e8c17c098552285e1", size = 35847826, upload-time = "2026-02-16T10:12:38.949Z" }, + { url = "https://files.pythonhosted.org/packages/54/a5/8cbc83f04aba433ca7b331b38f39e000efd9f0c7ce47128670e737542996/pyarrow-23.0.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0b95a3994f015be13c63148fef8832e8a23938128c185ee951c98908a696e0eb", size = 44536859, upload-time = "2026-02-16T10:12:45.467Z" }, + { url = "https://files.pythonhosted.org/packages/36/2e/c0f017c405fcdc252dbccafbe05e36b0d0eb1ea9a958f081e01c6972927f/pyarrow-23.0.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:4982d71350b1a6e5cfe1af742c53dfb759b11ce14141870d05d9e540d13bc5d1", size = 47614443, upload-time = "2026-02-16T10:12:55.525Z" }, + { url = "https://files.pythonhosted.org/packages/af/6b/2314a78057912f5627afa13ba43809d9d653e6630859618b0fd81a4e0759/pyarrow-23.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c250248f1fe266db627921c89b47b7c06fee0489ad95b04d50353537d74d6886", size = 48232991, upload-time = "2026-02-16T10:13:04.729Z" }, + { url = "https://files.pythonhosted.org/packages/40/f2/1bcb1d3be3460832ef3370d621142216e15a2c7c62602a4ea19ec240dd64/pyarrow-23.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f4763b83c11c16e5f4c15601ba6dfa849e20723b46aa2617cb4bffe8768479f", size = 50645077, upload-time = "2026-02-16T10:13:14.147Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3f/b1da7b61cd66566a4d4c8383d376c606d1c34a906c3f1cb35c479f59d1aa/pyarrow-23.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:3a4c85ef66c134161987c17b147d6bffdca4566f9a4c1d81a0a01cdf08414ea5", size = 28234271, upload-time = "2026-02-16T10:14:09.397Z" }, + { url = "https://files.pythonhosted.org/packages/b5/78/07f67434e910a0f7323269be7bfbf58699bd0c1d080b18a1ab49ba943fe8/pyarrow-23.0.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:17cd28e906c18af486a499422740298c52d7c6795344ea5002a7720b4eadf16d", size = 34488692, upload-time = "2026-02-16T10:13:21.541Z" }, + { url = "https://files.pythonhosted.org/packages/50/76/34cf7ae93ece1f740a04910d9f7e80ba166b9b4ab9596a953e9e62b90fe1/pyarrow-23.0.1-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:76e823d0e86b4fb5e1cf4a58d293036e678b5a4b03539be933d3b31f9406859f", size = 35964383, upload-time = "2026-02-16T10:13:28.63Z" }, + { url = "https://files.pythonhosted.org/packages/46/90/459b827238936d4244214be7c684e1b366a63f8c78c380807ae25ed92199/pyarrow-23.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a62e1899e3078bf65943078b3ad2a6ddcacf2373bc06379aac61b1e548a75814", size = 44538119, upload-time = "2026-02-16T10:13:35.506Z" }, + { url = "https://files.pythonhosted.org/packages/28/a1/93a71ae5881e99d1f9de1d4554a87be37da11cd6b152239fb5bd924fdc64/pyarrow-23.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:df088e8f640c9fae3b1f495b3c64755c4e719091caf250f3a74d095ddf3c836d", size = 47571199, upload-time = "2026-02-16T10:13:42.504Z" }, + { url = "https://files.pythonhosted.org/packages/88/a3/d2c462d4ef313521eaf2eff04d204ac60775263f1fb08c374b543f79f610/pyarrow-23.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:46718a220d64677c93bc243af1d44b55998255427588e400677d7192671845c7", size = 48259435, upload-time = "2026-02-16T10:13:49.226Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f1/11a544b8c3d38a759eb3fbb022039117fd633e9a7b19e4841cc3da091915/pyarrow-23.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a09f3876e87f48bc2f13583ab551f0379e5dfb83210391e68ace404181a20690", size = 50629149, upload-time = "2026-02-16T10:13:57.238Z" }, + { url = "https://files.pythonhosted.org/packages/50/f2/c0e76a0b451ffdf0cf788932e182758eb7558953f4f27f1aff8e2518b653/pyarrow-23.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:527e8d899f14bd15b740cd5a54ad56b7f98044955373a17179d5956ddb93d9ce", size = 28365807, upload-time = "2026-02-16T10:14:03.892Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pyqt5" +version = "5.15.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyqt5-qt5" }, + { name = "pyqt5-sip" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/07/c9ed0bd428df6f87183fca565a79fee19fa7c88c7f00a7f011ab4379e77a/PyQt5-5.15.11.tar.gz", hash = "sha256:fda45743ebb4a27b4b1a51c6d8ef455c4c1b5d610c90d2934c7802b5c1557c52", size = 3216775, upload-time = "2024-07-19T08:39:57.756Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/64/42ec1b0bd72d87f87bde6ceb6869f444d91a2d601f2e67cd05febc0346a1/PyQt5-5.15.11-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:c8b03dd9380bb13c804f0bdb0f4956067f281785b5e12303d529f0462f9afdc2", size = 6579776, upload-time = "2024-07-19T08:39:19.775Z" }, + { url = "https://files.pythonhosted.org/packages/49/f5/3fb696f4683ea45d68b7e77302eff173493ac81e43d63adb60fa760b9f91/PyQt5-5.15.11-cp38-abi3-macosx_11_0_x86_64.whl", hash = "sha256:6cd75628f6e732b1ffcfe709ab833a0716c0445d7aec8046a48d5843352becb6", size = 7016415, upload-time = "2024-07-19T08:39:32.977Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8c/4065950f9d013c4b2e588fe33cf04e564c2322842d84dbcbce5ba1dc28b0/PyQt5-5.15.11-cp38-abi3-manylinux_2_17_x86_64.whl", hash = "sha256:cd672a6738d1ae33ef7d9efa8e6cb0a1525ecf53ec86da80a9e1b6ec38c8d0f1", size = 8188103, upload-time = "2024-07-19T08:39:40.561Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f0/ae5a5b4f9b826b29ea4be841b2f2d951bcf5ae1d802f3732b145b57c5355/PyQt5-5.15.11-cp38-abi3-win32.whl", hash = "sha256:76be0322ceda5deecd1708a8d628e698089a1cea80d1a49d242a6d579a40babd", size = 5433308, upload-time = "2024-07-19T08:39:46.932Z" }, + { url = "https://files.pythonhosted.org/packages/56/d5/68eb9f3d19ce65df01b6c7b7a577ad3bbc9ab3a5dd3491a4756e71838ec9/PyQt5-5.15.11-cp38-abi3-win_amd64.whl", hash = "sha256:bdde598a3bb95022131a5c9ea62e0a96bd6fb28932cc1619fd7ba211531b7517", size = 6865864, upload-time = "2024-07-19T08:39:53.572Z" }, +] + +[[package]] +name = "pyqt5-qt5" +version = "5.15.18" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/90/bf01ac2132400997a3474051dd680a583381ebf98b2f5d64d4e54138dc42/pyqt5_qt5-5.15.18-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:8bb997eb903afa9da3221a0c9e6eaa00413bbeb4394d5706118ad05375684767", size = 39715743, upload-time = "2025-11-09T12:56:42.936Z" }, + { url = "https://files.pythonhosted.org/packages/24/8e/76366484d9f9dbe28e3bdfc688183433a7b82e314216e9b14c89e5fab690/pyqt5_qt5-5.15.18-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c656af9c1e6aaa7f59bf3d8995f2fa09adbf6762b470ed284c31dca80d686a26", size = 36798484, upload-time = "2025-11-09T12:56:59.998Z" }, + { url = "https://files.pythonhosted.org/packages/9a/46/ffe177f99f897a59dc237a20059020427bd2d3853d713992b8081933ddfe/pyqt5_qt5-5.15.18-py3-none-manylinux2014_x86_64.whl", hash = "sha256:bf2457e6371969736b4f660a0c153258fa03dbc6a181348218e6f05421682af7", size = 60864590, upload-time = "2025-11-09T12:57:26.724Z" }, +] + +[[package]] +name = "pyqt5-sip" +version = "12.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/31/5ef342de9faee0f3801088946ae103db9b9eaeba3d6a64fefd5ce74df244/pyqt5_sip-12.18.0.tar.gz", hash = "sha256:71c37db75a0664325de149f43e2a712ec5fa1f90429a21dafbca005cb6767f94", size = 104143, upload-time = "2026-01-13T15:53:19.576Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/59/3dd29bcfde479ac241f618235bf7d76e65e47afdcd91743554d490ae0d19/pyqt5_sip-12.18.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13c32e9025d0ab5fe960ef64dcb4c6f7ec26156ddce2cf2f195600aa26e8f9fe", size = 122724, upload-time = "2026-01-13T15:52:49.875Z" }, + { url = "https://files.pythonhosted.org/packages/87/c4/ac4deee3249d3ceb703103acbbf76d89f3782fa7ad2ca5a15fcb5bcf5c73/pyqt5_sip-12.18.0-cp311-cp311-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dd855149c724634eb1f92d10e04e1be0751068a8521d5f9f06e5c2dbe32fd89f", size = 327560, upload-time = "2026-01-13T15:52:53.584Z" }, + { url = "https://files.pythonhosted.org/packages/79/53/64373ba9311288b0ea6b5ab375d9c9743a41ac93df7137655498c95a08e5/pyqt5_sip-12.18.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:521729e4ce44db555005f4e6987c4a5164d45ea03f5a7d08519813d4776acd15", size = 277067, upload-time = "2026-01-13T15:52:51.22Z" }, + { url = "https://files.pythonhosted.org/packages/ce/dd/4026bba8355ceaba222db0369fe4a480d55aa61d3f14dbe1458886ccc032/pyqt5_sip-12.18.0-cp311-cp311-win32.whl", hash = "sha256:c738949863d88b78f86f28e13151989ca3b1a302934811af41856dc8a27838bd", size = 49323, upload-time = "2026-01-13T15:52:55.859Z" }, + { url = "https://files.pythonhosted.org/packages/41/35/39e549ca7f7c9fbb025912a5db7f55f3b9c22d7f9718f41c2e7a17c806e9/pyqt5_sip-12.18.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac0be1a8ce145ed78c7e8d45243749405bee7e3a87e9810b0542bca010c50bd7", size = 58795, upload-time = "2026-01-13T15:52:54.995Z" }, + { url = "https://files.pythonhosted.org/packages/2a/61/6d78d702016ac23d2b97634a3b6a831c3f7735f0552a1c8b058db96005d1/pyqt5_sip-12.18.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b29e4cda24748e59e5bd1bdad4812091a86b4b5b08c38b7f781eb55a5166f2b7", size = 124614, upload-time = "2026-01-13T15:52:57.59Z" }, + { url = "https://files.pythonhosted.org/packages/19/bf/8f3efa10ddd3e76c1253865340ab7c2960ef96681d732b1f666c77430612/pyqt5_sip-12.18.0-cp312-cp312-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:163c2bba5e637c2222ec17d82a2c5aa158184a191923eb7d137cf4cfa0399529", size = 339412, upload-time = "2026-01-13T15:53:00.563Z" }, + { url = "https://files.pythonhosted.org/packages/72/48/f1bcf6729d01bae6729cd790b22fd579dbe34014e8be031e6f10c5b9b2aa/pyqt5_sip-12.18.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ead5e0a64ad852ac60797989d8444a6a5bd834768536b04a07b40b2937d922f6", size = 282376, upload-time = "2026-01-13T15:52:59.172Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b7/d84c764ac9f1366be561255ec9bd88ee224fefdbdb349aee250f3003f0ca/pyqt5_sip-12.18.0-cp312-cp312-win32.whl", hash = "sha256:993fe3ed9a62a92e770f32d5344e3df56c2cacf1471f01b7feaf04818a2df1c4", size = 49523, upload-time = "2026-01-13T15:53:03.068Z" }, + { url = "https://files.pythonhosted.org/packages/ab/e7/ef87178d5afa5f63be38556dc0df8af89f9bf74f2555f4dab6824c0fd150/pyqt5_sip-12.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:9b689e02e400abd1ce0a30cd6eae8eceabcf1bbba0395cb5c86e64ba74351d68", size = 58001, upload-time = "2026-01-13T15:53:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/79/67/8d43d0fea10ff48ddecc8534aead8b855dc80df80653b8b1bf9e1f993063/pyqt5_sip-12.18.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9254e5dd7676b76503ba20edcc919e7ac4a97b6c70a6fb2f9dba9e13b4c60509", size = 124605, upload-time = "2026-01-13T15:53:04.991Z" }, + { url = "https://files.pythonhosted.org/packages/48/2a/b08bc8efeb49c50c6cdac11417dc2c8eaefcac2f0a6382eae7b26dc0f232/pyqt5_sip-12.18.0-cp313-cp313-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c969631ada7293a81e1012b2264a62d69a91995b517586489dfe24421b87b9af", size = 339918, upload-time = "2026-01-13T15:53:08.502Z" }, + { url = "https://files.pythonhosted.org/packages/b6/99/24f82437b2f073cf39296b7c731b6a8bc0f5207911fdd93841a0ea9abe42/pyqt5_sip-12.18.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d84ac384a63285132e67762c87681191c25e28a1df7560287ec3889d9eb223b5", size = 282088, upload-time = "2026-01-13T15:53:06.632Z" }, + { url = "https://files.pythonhosted.org/packages/3e/27/20d3924943df34361fae9c6a0489ae89d0b07571693245c61678d185e4a4/pyqt5_sip-12.18.0-cp313-cp313-win32.whl", hash = "sha256:95bba4670ecf5cba73958b85aa2087c17838a402ed251c38e68060c7665c998b", size = 49501, upload-time = "2026-01-13T15:53:11.159Z" }, + { url = "https://files.pythonhosted.org/packages/3f/36/e251623c12968730730512a9e5150430e36246afbe64894610190b896f61/pyqt5_sip-12.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:aac4adc37df2f2ac1dc259409be1900f07332d140a12c9db7c84112cef64ff59", size = 58076, upload-time = "2026-01-13T15:53:09.928Z" }, + { url = "https://files.pythonhosted.org/packages/37/3a/b46a0116b1aacbb6156b2957eb5cb928c94b49f4626eb2540ca8d16ee757/pyqt5_sip-12.18.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8372ec8704bfd5e09942d0d055a1657eb4f702f4b30847a5e59df0496f99d67f", size = 124594, upload-time = "2026-01-13T15:53:13.159Z" }, + { url = "https://files.pythonhosted.org/packages/58/63/df3037f11391c25c5b0ab233d22e58b8f056cb1ce16d7ecadb844421ce75/pyqt5_sip-12.18.0-cp314-cp314-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fdb45c7cd2af7eccd7370b994d432bfc7965079f845392760724f26771bb59dc", size = 339056, upload-time = "2026-01-13T15:53:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e7/4f96b84520b8f8b7502682fd43f68f63ca6572b5858f56e5f61c76a54fe2/pyqt5_sip-12.18.0-cp314-cp314-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:92abe984becbde768954d6d0951f56d80a9868d2fd9e738e61fc944f0ff83dd6", size = 282439, upload-time = "2026-01-13T15:53:14.856Z" }, + { url = "https://files.pythonhosted.org/packages/79/8e/ccdf20d373ceba83e1d1b7f818505c375208ffde4a96376dc7dbe592406c/pyqt5_sip-12.18.0-cp314-cp314-win32.whl", hash = "sha256:bd9e3c6f81346f1b08d6db02305cdee20c009b43aa083d44ee2de47a7da0e123", size = 50713, upload-time = "2026-01-13T15:53:18.634Z" }, + { url = "https://files.pythonhosted.org/packages/7f/21/8486ed45977be615ec5371b24b47298b1cb0e1a455b419eddd0215078dba/pyqt5_sip-12.18.0-cp314-cp314-win_amd64.whl", hash = "sha256:6d948f1be619c645cd3bda54952bfdc1aef7c79242dccea6a6858748e61114b9", size = 59622, upload-time = "2026-01-13T15:53:17.714Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "pytest-qt" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pluggy" }, + { name = "pytest" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/61/8bdec02663c18bf5016709b909411dce04a868710477dc9b9844ffcf8dd2/pytest_qt-4.5.0.tar.gz", hash = "sha256:51620e01c488f065d2036425cbc1cbcf8a6972295105fd285321eb47e66a319f", size = 128702, upload-time = "2025-07-01T17:24:39.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/d0/8339b888ad64a3d4e508fed8245a402b503846e1972c10ad60955883dcbb/pytest_qt-4.5.0-py3-none-any.whl", hash = "sha256:ed21ea9b861247f7d18090a26bfbda8fb51d7a8a7b6f776157426ff2ccf26eff", size = 37214, upload-time = "2025-07-01T17:24:38.226Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pyvisa" +version = "1.16.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/72/a50adff945e66f2161a44b243970f809de19152a2e7da0473ae8ff96d642/pyvisa-1.16.2.tar.gz", hash = "sha256:75beb93eeafe20a50be5726fa4e3a645948c93d86819f9c1f8c542efa4085a38", size = 238729, upload-time = "2026-02-27T12:14:08.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/a9/776da4f397003cca093659524c3526590522f81b642936838428c11274e6/pyvisa-1.16.2-py3-none-any.whl", hash = "sha256:54f034adafd3e8d1858d57cdafec64e920444f4b84b31c9fd17487fbad0a197a", size = 181413, upload-time = "2026-02-27T12:14:07.301Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "pyzmq" +version = "27.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, + { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, + { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, + { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, + { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, + { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, + { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, + { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, + { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, + { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, +] + +[[package]] +name = "qcodes" +version = "0.55.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "broadbean" }, + { name = "cf-xarray" }, + { name = "dask" }, + { name = "h5netcdf" }, + { name = "h5py" }, + { name = "ipykernel" }, + { name = "ipywidgets" }, + { name = "jsonschema" }, + { name = "matplotlib" }, + { name = "networkx" }, + { name = "numpy" }, + { name = "opentelemetry-api" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "pyarrow" }, + { name = "pyvisa" }, + { name = "ruamel-yaml" }, + { name = "tabulate" }, + { name = "tqdm" }, + { name = "typing-extensions" }, + { name = "uncertainties" }, + { name = "versioningit" }, + { name = "websockets" }, + { name = "xarray" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/f6/b482fe84ff76cab62c7cf967ef658b4958f384d065f89b238a9b9e618f7a/qcodes-0.55.0.tar.gz", hash = "sha256:56d07628a00087546b750ef3f92f503362f0e8a70e1685f887771e074f9293e2", size = 825296, upload-time = "2026-02-25T12:18:09.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/2c/687cd527aba1594148692a962661612f90a74a1b305e4d10ded8adaa93a8/qcodes-0.55.0-py3-none-any.whl", hash = "sha256:e5b4db0c498020b765821221cca7c50b5194f365a511b65f466c819af33acd38", size = 984458, upload-time = "2026-02-25T12:18:07.889Z" }, +] + +[[package]] +name = "qtpy" +version = "2.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/01/392eba83c8e47b946b929d7c46e0f04b35e9671f8bb6fc36b6f7945b4de8/qtpy-2.4.3.tar.gz", hash = "sha256:db744f7832e6d3da90568ba6ccbca3ee2b3b4a890c3d6fbbc63142f6e4cdf5bb", size = 66982, upload-time = "2025-02-11T15:09:25.759Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/76/37c0ccd5ab968a6a438f9c623aeecc84c202ab2fabc6a8fd927580c15b5a/QtPy-2.4.3-py3-none-any.whl", hash = "sha256:72095afe13673e017946cc258b8d5da43314197b741ed2890e563cf384b51aa1", size = 95045, upload-time = "2025-02-11T15:09:24.162Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "ruamel-yaml" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" }, +] + +[[package]] +name = "schema" +version = "0.7.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/2e/8da627b65577a8f130fe9dfa88ce94fcb24b1f8b59e0fc763ee61abef8b8/schema-0.7.8.tar.gz", hash = "sha256:e86cc08edd6fe6e2522648f4e47e3a31920a76e82cce8937535422e310862ab5", size = 45540, upload-time = "2025-10-11T13:15:40.281Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/75/aad85817266ac5285c93391711d231ca63e9ae7d42cd3ca37549e24ebe52/schema-0.7.8-py2.py3-none-any.whl", hash = "sha256:00bd977fadc7d9521bf289850cd8a8aa5f4948f575476b8daaa5c1b57af2dce1", size = 19108, upload-time = "2025-10-11T17:13:07.323Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" }, + { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" }, + { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" }, + { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" }, + { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" }, + { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, +] + +[[package]] +name = "tabulate" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", size = 91754, upload-time = "2026-03-04T18:55:34.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" }, +] + +[[package]] +name = "toolz" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/d6/114b492226588d6ff54579d95847662fc69196bdeec318eb45393b24c192/toolz-1.1.0.tar.gz", hash = "sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b", size = 52613, upload-time = "2025-10-17T04:03:21.661Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl", hash = "sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8", size = 58093, upload-time = "2025-10-17T04:03:20.435Z" }, +] + +[[package]] +name = "tornado" +version = "6.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/f1/3173dfa4a18db4a9b03e5d55325559dab51ee653763bb8745a75af491286/tornado-6.5.5.tar.gz", hash = "sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9", size = 516006, upload-time = "2026-03-10T21:31:02.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa", size = 445983, upload-time = "2026-03-10T21:30:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5e/7625b76cd10f98f1516c36ce0346de62061156352353ef2da44e5c21523c/tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521", size = 444246, upload-time = "2026-03-10T21:30:46.571Z" }, + { url = "https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5", size = 447229, upload-time = "2026-03-10T21:30:48.273Z" }, + { url = "https://files.pythonhosted.org/packages/34/01/74e034a30ef59afb4097ef8659515e96a39d910b712a89af76f5e4e1f93c/tornado-6.5.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07", size = 448192, upload-time = "2026-03-10T21:30:51.22Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/fe9e02c5a96429fce1a1d15a517f5d8444f9c412e0bb9eadfbe3b0fc55bf/tornado-6.5.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e", size = 448039, upload-time = "2026-03-10T21:30:53.52Z" }, + { url = "https://files.pythonhosted.org/packages/82/9e/656ee4cec0398b1d18d0f1eb6372c41c6b889722641d84948351ae19556d/tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca", size = 447445, upload-time = "2026-03-10T21:30:55.541Z" }, + { url = "https://files.pythonhosted.org/packages/5a/76/4921c00511f88af86a33de770d64141170f1cfd9c00311aea689949e274e/tornado-6.5.5-cp39-abi3-win32.whl", hash = "sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7", size = 448582, upload-time = "2026-03-10T21:30:57.142Z" }, + { url = "https://files.pythonhosted.org/packages/2c/23/f6c6112a04d28eed765e374435fb1a9198f73e1ec4b4024184f21faeb1ad/tornado-6.5.5-cp39-abi3-win_amd64.whl", hash = "sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b", size = 448990, upload-time = "2026-03-10T21:30:58.857Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c8/876602cbc96469911f0939f703453c1157b0c826ecb05bdd32e023397d4e/tornado-6.5.5-cp39-abi3-win_arm64.whl", hash = "sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6", size = 448016, upload-time = "2026-03-10T21:31:00.43Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + +[[package]] +name = "uncertainties" +version = "3.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/0c/cb09f33b26955399c675ab378e4063ed7e48422d3d49f96219ab0be5eba9/uncertainties-3.2.3.tar.gz", hash = "sha256:76a5653e686f617a42922d546a239e9efce72e6b35411b7750a1d12dcba03031", size = 160492, upload-time = "2025-04-21T19:58:28.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl", hash = "sha256:313353900d8f88b283c9bad81e7d2b2d3d4bcc330cbace35403faaed7e78890a", size = 60118, upload-time = "2025-04-21T19:58:26.864Z" }, +] + +[[package]] +name = "versioningit" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/f4/bc578cc80989c572231a36cc03cc097091176fa3fb8b4e2af1deb4370eb7/versioningit-3.3.0.tar.gz", hash = "sha256:b91ad7d73e73d21220e69540f20213f2b729a1f9b35c04e9e137eaf28d2214da", size = 220280, upload-time = "2025-06-27T20:13:23.368Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl", hash = "sha256:23b1db3c4756cded9bd6b0ddec6643c261e3d0c471707da3e0b230b81ce53e4b", size = 38439, upload-time = "2025-06-27T20:13:21.927Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + +[[package]] +name = "widgetsnbextension" +version = "4.0.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/f4/c67440c7fb409a71b7404b7aefcd7569a9c0d6bd071299bf4198ae7a5d95/widgetsnbextension-4.0.15.tar.gz", hash = "sha256:de8610639996f1567952d763a5a41af8af37f2575a41f9852a38f947eb82a3b9", size = 1097402, upload-time = "2025-11-01T21:15:55.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl", hash = "sha256:8156704e4346a571d9ce73b84bee86a29906c9abfd7223b7228a28899ccf3366", size = 2196503, upload-time = "2025-11-01T21:15:53.565Z" }, +] + +[[package]] +name = "xarray" +version = "2026.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/03/e3353b72e518574b32993989d8f696277bf878e9d508c7dd22e86c0dab5b/xarray-2026.2.0.tar.gz", hash = "sha256:978b6acb018770554f8fd964af4eb02f9bcc165d4085dbb7326190d92aa74bcf", size = 3111388, upload-time = "2026-02-13T22:20:50.18Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/92/545eb2ca17fc0e05456728d7e4378bfee48d66433ae3b7e71948e46826fb/xarray-2026.2.0-py3-none-any.whl", hash = "sha256:e927d7d716ea71dea78a13417970850a640447d8dd2ceeb65c5687f6373837c9", size = 1405358, upload-time = "2026-02-13T22:20:47.847Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] From fe3980f181c2cfdad1203d7e5e9fc9b920eeb0d6 Mon Sep 17 00:00:00 2001 From: marcosf2 Date: Wed, 22 Apr 2026 16:32:32 -0300 Subject: [PATCH 03/12] harden ZMQ/Qt teardown and prevent cross-test instrument leaks --- src/instrumentserver/client/core.py | 25 ++++++++++++++++--- src/instrumentserver/client/proxy.py | 9 +++++++ src/instrumentserver/server/application.py | 14 ++++++++++- src/instrumentserver/server/core.py | 13 ++++++++-- .../testing/dummy_instruments/generic.py | 8 ++++++ test/pytest/conftest.py | 18 ++++++++++--- 6 files changed, 78 insertions(+), 9 deletions(-) diff --git a/src/instrumentserver/client/core.py b/src/instrumentserver/client/core.py index a8dfa62..6b0cd8c 100644 --- a/src/instrumentserver/client/core.py +++ b/src/instrumentserver/client/core.py @@ -27,6 +27,7 @@ class BaseClient: def __init__(self, host='localhost', port=DEFAULT_PORT, connect=True, timeout=20, raise_exceptions=True): self.connected = False + self._closed = False self.context = None self.socket = None self.host = host @@ -47,6 +48,22 @@ def __exit__(self, exc_type, exc_val, exc_tb): self.disconnect() def connect(self): + if self._closed: + raise RuntimeError("Client has been permanently disconnected.") + # Clean up any existing context/socket so we don't leak them when + # connect() is called more than once (e.g. EmbeddedClient.start()). + if self.socket is not None: + try: + self.socket.close(linger=0) + except Exception: + pass + self.socket = None + if self.context is not None: + try: + self.context.destroy(linger=0) + except Exception: + pass + self.context = None logger.info(f"Connecting to {self.addr}") self.context = zmq.Context() self.socket = self.context.socket(zmq.DEALER) @@ -56,7 +73,7 @@ def connect(self): self.connected = True def ask(self, message): - if not self.connected: + if self._closed or not self.connected: raise RuntimeError("No connection yet.") # try so that if timeout happens, the client remains usable @@ -87,7 +104,8 @@ def _reset_connection(self): self.socket.close(linger=0) finally: self.connected = False - self.connect() + if not self._closed: + self.connect() def _handle_server_error(self, err): if isinstance(err, str): @@ -107,6 +125,7 @@ def _handle_server_error(self, err): logger.error(msg) def disconnect(self): + self._closed = True if self.socket is not None: try: self.socket.close(linger=0) @@ -115,7 +134,7 @@ def disconnect(self): self.socket = None if self.context is not None: try: - self.context.term() + self.context.destroy(linger=0) except Exception: pass self.context = None diff --git a/src/instrumentserver/client/proxy.py b/src/instrumentserver/client/proxy.py index 1de2862..cd92626 100644 --- a/src/instrumentserver/client/proxy.py +++ b/src/instrumentserver/client/proxy.py @@ -704,6 +704,15 @@ def __init__(self, parent=None, _QtAdapter.__init__(self, parent=parent) Client.__init__(self, host, port, connect, timeout, raise_exceptions) + def disconnect(self, *args, **kwargs): + # QObject.disconnect() shadows BaseClient.disconnect() via MRO, so + # explicitly dispatch to BaseClient.disconnect() when called without + # Qt signal arguments. Preserve Qt's signal-disconnect semantics when + # invoked with signal arguments (e.g. disconnect(signal, slot)). + if not args and not kwargs: + return Client.disconnect(self) + return _QtAdapter.disconnect(self, *args, **kwargs) + class ClientStation: def __init__(self, host='localhost', port=DEFAULT_PORT, connect=True, timeout=20, raise_exceptions=True, diff --git a/src/instrumentserver/server/application.py b/src/instrumentserver/server/application.py index 2b2b4fa..1d88e94 100644 --- a/src/instrumentserver/server/application.py +++ b/src/instrumentserver/server/application.py @@ -603,6 +603,11 @@ def closeEvent(self, event): self.client.ask(self.stationServer.SAFEWORD) except Exception: pass + + try: + self.client.disconnect() + except Exception: + pass event.accept() def startServer(self): @@ -682,8 +687,13 @@ def removeInstrumentFromGui(self, name: str): def refreshStationComponents(self): """Clear and re-populate the widget holding the station components, using the objects that are currently registered in the station.""" + if getattr(self.client, '_closed', False) or not self.client.connected: + return self.stationList.clear() - instruments = self.client.list_instruments() + try: + instruments = self.client.list_instruments() + except RuntimeError: + return if not instruments: return for ins in instruments: @@ -879,6 +889,8 @@ class EmbeddedClient(QtClient): @QtCore.Slot(str) def start(self, addr: str): + if self._closed: + return self.addr = "tcp://localhost:" + addr.split(':')[-1] self.connect() diff --git a/src/instrumentserver/server/core.py b/src/instrumentserver/server/core.py index 1d6b3c9..5821bd1 100644 --- a/src/instrumentserver/server/core.py +++ b/src/instrumentserver/server/core.py @@ -264,10 +264,19 @@ def startServer(self) -> bool: logger.exception(f"Unexpected error in server loop: {e}") break - socket.close() + socket.close(linger=0) self._wakeup_r.close() self._wakeup_w.close() - self.broadcastSocket.close() + self.broadcastSocket.close(linger=0) + if self.externalBroadcastSocket is not None: + try: + self.externalBroadcastSocket.close(linger=0) + except Exception: + pass + try: + context.destroy(linger=0) + except Exception: + pass self.finished.emit() logger.info("StationServer shut down cleanly.") return True diff --git a/src/instrumentserver/testing/dummy_instruments/generic.py b/src/instrumentserver/testing/dummy_instruments/generic.py index 5f31353..bde62de 100644 --- a/src/instrumentserver/testing/dummy_instruments/generic.py +++ b/src/instrumentserver/testing/dummy_instruments/generic.py @@ -60,6 +60,14 @@ def __init__(self, name: str, address=None, first_arg=None, second_arg=None, *ar channel = DummyChannel(f'{name}_Chan{chan_name}') self.add_submodule(chan_name, channel) + def close(self) -> None: + for submodule in list(getattr(self, 'submodules', {}).values()): + try: + submodule.close() + except Exception: + pass + super().close() + def ask_raw(self, cmd): """Dummy ask_raw so *IDN? and similar SCPI queries don't explode the GUI.""" if cmd.strip().upper().startswith('*IDN'): diff --git a/test/pytest/conftest.py b/test/pytest/conftest.py index 5d2bb98..9667a65 100644 --- a/test/pytest/conftest.py +++ b/test/pytest/conftest.py @@ -1,11 +1,26 @@ import instrumentserver.testing.dummy_instruments.generic import pytest # type: ignore[import-not-found] +import qcodes as qc from instrumentserver.server.core import startServer from instrumentserver.client.core import BaseClient from instrumentserver.client.proxy import Client +@pytest.fixture(autouse=True, scope='module') +def _close_instruments_between_modules(): + """Ensure every test module starts with a clean qcodes instrument registry. + + qcodes.Instrument._all_instruments is a class-level weakref dict that + persists for the entire pytest session. Instruments left behind by one + module (e.g. channel submodules, which qcodes.Instrument.close() does not + cascade-close in this qcodes version) cause KeyError collisions when the + next module tries to create an instrument with the same name. + """ + yield + qc.Instrument.close_all() + + @pytest.fixture(scope='session') def qapp_session(): """Ensure a QApplication exists for the entire test session. @@ -54,6 +69,3 @@ def dummy_instrument(cli): def param_manager(cli): params = cli.find_or_create_instrument('parameter_manager', 'instrumentserver.params.ParameterManager') return cli, params - - - From dd9c23fc1cfa5ea3e1ec0dcb2f895a2fb8234a9d Mon Sep 17 00:00:00 2001 From: marcosf2 Date: Fri, 24 Apr 2026 12:15:41 -0300 Subject: [PATCH 04/12] Updating dependencies --- .github/workflows/sphinx-docs.yml | 26 +- environment-docs.yml | 21 - pyproject.toml | 66 +- src/instrumentserver/monitoring/listener.py | 7 +- uv.lock | 1099 ++++++++++++++++++- 5 files changed, 1174 insertions(+), 45 deletions(-) delete mode 100644 environment-docs.yml diff --git a/.github/workflows/sphinx-docs.yml b/.github/workflows/sphinx-docs.yml index bd323f8..ee109d0 100644 --- a/.github/workflows/sphinx-docs.yml +++ b/.github/workflows/sphinx-docs.yml @@ -2,7 +2,11 @@ name: "Sphinx: Render docs" on: push: + branches: + - master pull_request: + branches: + - master jobs: build: @@ -14,20 +18,16 @@ jobs: with: persist-credentials: false - - name: Set up conda - uses: conda-incubator/setup-miniconda@v3 + - name: Set up uv + uses: astral-sh/setup-uv@v3 with: - auto-activate-base: true - activate-environment: instrumentserver-docs - environment-file: environment-docs.yml - - - name: Install instrumentserver package - shell: bash -l {0} - run: pip install -e . - + enable-cache: true + - name: Install system deps + run: sudo apt-get update && sudo apt-get install -y pandoc + - name: Install dependencies + run: uv sync --frozen --group docs - name: Build HTML - shell: bash -l {0} - run: cd docs && make html + run: uv run --directory docs -- make html - name: Upload artifacts uses: actions/upload-artifact@v4 @@ -40,4 +40,4 @@ jobs: if: github.ref == 'refs/heads/master' with: github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: docs/build/ \ No newline at end of file + publish_dir: docs/build/ diff --git a/environment-docs.yml b/environment-docs.yml deleted file mode 100644 index ec293e2..0000000 --- a/environment-docs.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: instrumentserver-docs -channels: - - conda-forge - - defaults -dependencies: - - python=3.10 - - sphinx>=5.0 - - sphinx-autobuild>=2021.3.20 - - pydata-sphinx-theme>=0.13.0 - - myst-parser>=0.18.0 - - linkify-it-py - - sphinx-autodoc-typehints>=1.19.0 - - nbsphinx>=0.8.0 - - nbconvert>=7.0.0 - - jupyter>=1.0.0 - - ipython>=7.0.0 - - scipy>=1.0 - - pandas>=1.0 - - xarray>=0.16 - - packaging>=20.0 - - pip \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index c8218d3..74e4945 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,9 +18,17 @@ dependencies = [ "pyzmq", "qcodes", "qtpy", - "scipy" + "scipy", + "numpy", + "pandas", + "jsonschema", + "ruamel.yaml", + "PyYAML", ] +[project.optional-dependencies] +monitoring = ["influxdb-client"] + [project.urls] Homepage = "https://github.com/toolsforexperiments/instrumentserver" @@ -37,12 +45,68 @@ package-dir = {"" = "src"} [tool.setuptools.packages.find] where = ["src"] +[tool.ruff] +exclude = ["docs"] + +[tool.ruff.lint] +extend-select = ["I"] + +[tool.mypy] +files = ["src"] +strict_optional = true +show_column_numbers = true +warn_unused_ignores = true +warn_unused_configs = true +warn_redundant_casts = true +no_implicit_optional = true +disallow_untyped_defs = true +show_error_codes = true +enable_error_code = "ignore-without-code" + +[[tool.mypy.overrides]] +module = [ + "PyQt5", + "PyQt5.*", + "qcodes", + "qcodes.*", + "qtpy", + "qtpy.*", + "pyqtgraph", + "pyqtgraph.*", + "scipy", + "scipy.*", + "zmq", + "zmq.*", + "ruamel", + "ruamel.*", + "jsonschema", + "jsonschema.*", + "yaml", + "pandas", + "pandas.*", +] +ignore_missing_imports = true + [tool.pytest.ini_options] testpaths = ["test/pytest"] qt_api = "pyqt5" +log_cli = true + +[tool.coverage.run] +branch = true [dependency-groups] dev = [ "pytest>=9.0.2", "pytest-qt>=4.5.0", + "pytest-cov", + "ruff", + "mypy", +] +docs = [ + "sphinx", + "pydata-sphinx-theme", + "myst-parser", + "nbsphinx", + "linkify-it-py", ] diff --git a/src/instrumentserver/monitoring/listener.py b/src/instrumentserver/monitoring/listener.py index cd06e23..94667e5 100644 --- a/src/instrumentserver/monitoring/listener.py +++ b/src/instrumentserver/monitoring/listener.py @@ -4,11 +4,11 @@ from abc import ABC, abstractmethod from dataclasses import dataclass from datetime import datetime, timedelta, timezone +from zoneinfo import ZoneInfo, ZoneInfoNotFoundError from pathlib import Path from typing import Any, Dict import pandas as pd -import pytz import ruamel.yaml # type: ignore[import-untyped] # Known bugfix under no-fix status: https://sourceforge.net/p/ruamel-yaml/tickets/328/ import zmq try: @@ -187,9 +187,8 @@ def checkCSVConfig(configInput: Dict[str, Any]): def get_timezone_info(timezone_name): try: - tz = pytz.timezone(timezone_name) - return tz - except pytz.UnknownTimeZoneError: + return ZoneInfo(timezone_name) + except ZoneInfoNotFoundError: print(f"Unknown timezone: {timezone_name}") return None diff --git a/uv.lock b/uv.lock index 9dc9a10..16648aa 100644 --- a/uv.lock +++ b/uv.lock @@ -2,9 +2,12 @@ version = 1 revision = 3 requires-python = ">=3.11" resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version < '3.12' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", @@ -13,6 +16,27 @@ resolution-markers = [ "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] +[[package]] +name = "accessible-pygments" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899, upload-time = "2024-05-10T11:23:10.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" }, +] + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, +] + [[package]] name = "appnope" version = "0.1.4" @@ -40,6 +64,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, +] + +[[package]] +name = "bleach" +version = "6.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/18/3c8523962314be6bf4c8989c79ad9531c825210dd13a8669f6b84336e8bd/bleach-6.3.0.tar.gz", hash = "sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22", size = 203533, upload-time = "2025-10-27T17:57:39.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl", hash = "sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6", size = 164437, upload-time = "2025-10-27T17:57:37.538Z" }, +] + +[package.optional-dependencies] +css = [ + { name = "tinycss2" }, +] + [[package]] name = "broadbean" version = "0.14.0" @@ -55,6 +118,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/48/4d/2b3b4b35456176182d45cdf977fdea80bf71be563f4074536ec3436eed9c/broadbean-0.14.0-py3-none-any.whl", hash = "sha256:7a9195ef16241853e2ea20aedc6f67ee72f5464a463b3584fcbedcb63daf88e7", size = 36755, upload-time = "2024-03-06T22:15:42.41Z" }, ] +[[package]] +name = "certifi" +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, +] + [[package]] name = "cf-xarray" version = "0.10.11" @@ -137,6 +209,95 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + [[package]] name = "click" version = "8.3.1" @@ -258,6 +419,110 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, ] +[[package]] +name = "coverage" +version = "7.13.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/37/d24c8f8220ff07b839b2c043ea4903a33b0f455abe673ae3c03bbdb7f212/coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d", size = 219381, upload-time = "2026-03-17T10:30:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587", size = 219880, upload-time = "2026-03-17T10:30:16.231Z" }, + { url = "https://files.pythonhosted.org/packages/55/2f/e0e5b237bffdb5d6c530ce87cc1d413a5b7d7dfd60fb067ad6d254c35c76/coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642", size = 250303, upload-time = "2026-03-17T10:30:17.748Z" }, + { url = "https://files.pythonhosted.org/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b", size = 252218, upload-time = "2026-03-17T10:30:19.804Z" }, + { url = "https://files.pythonhosted.org/packages/da/69/2f47bb6fa1b8d1e3e5d0c4be8ccb4313c63d742476a619418f85740d597b/coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686", size = 254326, upload-time = "2026-03-17T10:30:21.321Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d0/79db81da58965bd29dabc8f4ad2a2af70611a57cba9d1ec006f072f30a54/coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743", size = 256267, upload-time = "2026-03-17T10:30:23.094Z" }, + { url = "https://files.pythonhosted.org/packages/e5/32/d0d7cc8168f91ddab44c0ce4806b969df5f5fdfdbb568eaca2dbc2a04936/coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75", size = 250430, upload-time = "2026-03-17T10:30:25.311Z" }, + { url = "https://files.pythonhosted.org/packages/4d/06/a055311d891ddbe231cd69fdd20ea4be6e3603ffebddf8704b8ca8e10a3c/coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209", size = 252017, upload-time = "2026-03-17T10:30:27.284Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f6/d0fd2d21e29a657b5f77a2fe7082e1568158340dceb941954f776dce1b7b/coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a", size = 250080, upload-time = "2026-03-17T10:30:29.481Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ab/0d7fb2efc2e9a5eb7ddcc6e722f834a69b454b7e6e5888c3a8567ecffb31/coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e", size = 253843, upload-time = "2026-03-17T10:30:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/ba/6f/7467b917bbf5408610178f62a49c0ed4377bb16c1657f689cc61470da8ce/coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd", size = 249802, upload-time = "2026-03-17T10:30:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/75/2c/1172fb689df92135f5bfbbd69fc83017a76d24ea2e2f3a1154007e2fb9f8/coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8", size = 250707, upload-time = "2026-03-17T10:30:35.2Z" }, + { url = "https://files.pythonhosted.org/packages/67/21/9ac389377380a07884e3b48ba7a620fcd9dbfaf1d40565facdc6b36ec9ef/coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf", size = 221880, upload-time = "2026-03-17T10:30:36.775Z" }, + { url = "https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9", size = 222816, upload-time = "2026-03-17T10:30:38.891Z" }, + { url = "https://files.pythonhosted.org/packages/12/a6/1d3f6155fb0010ca68eba7fe48ca6c9da7385058b77a95848710ecf189b1/coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028", size = 221483, upload-time = "2026-03-17T10:30:40.463Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, + { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, + { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, + { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, + { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, + { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, + { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, + { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, + { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, + { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + [[package]] name = "cycler" version = "0.12.1" @@ -320,6 +585,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, ] +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + [[package]] name = "executing" version = "2.2.1" @@ -329,6 +612,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, ] +[[package]] +name = "fastjsonschema" +version = "2.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de", size = 374130, upload-time = "2025-08-14T18:49:36.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, +] + [[package]] name = "fonttools" version = "4.62.1" @@ -451,6 +743,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/96/91/9fad90cfc5f9b2489c7c26ad897157bce82f0e9534a986a221b99760b23b/h5py-3.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:faca8fb4e4319c09d83337adc80b2ca7d5c5a343c2d6f1b6388f32cfecca13c1", size = 2740706, upload-time = "2026-03-06T13:49:06.347Z" }, ] +[[package]] +name = "idna" +version = "3.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, +] + +[[package]] +name = "imagesize" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, +] + [[package]] name = "importlib-metadata" version = "8.7.1" @@ -463,6 +773,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, ] +[[package]] +name = "influxdb-client" +version = "1.50.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "python-dateutil" }, + { name = "reactivex" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/23/77a945465a556a8067917ba84bac04e312cee2a4fe03d41198fe79ed9c84/influxdb_client-1.50.0.tar.gz", hash = "sha256:c2a4906573097103fa6f9a2ab08efe2eb48c2fed60b8129a3e320affde445743", size = 386749, upload-time = "2026-01-23T09:39:39.322Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/ec/6b120b4a86f6fadc7ddb1d7c7cdcb15bcfb332deb022ab60df51bcd4494c/influxdb_client-1.50.0-py3-none-any.whl", hash = "sha256:f172975cf7f0c95bfe74f288b31273393b164d2c58a948de55497d9956ab49be", size = 746289, upload-time = "2026-01-23T09:39:37.377Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -477,32 +802,70 @@ name = "instrumentserver" version = "0.0.1" source = { editable = "." } dependencies = [ + { name = "jsonschema" }, + { name = "numpy" }, + { name = "pandas" }, { name = "pyqt5" }, + { name = "pyyaml" }, { name = "pyzmq" }, { name = "qcodes" }, { name = "qtpy" }, + { name = "ruamel-yaml" }, { name = "scipy" }, ] +[package.optional-dependencies] +monitoring = [ + { name = "influxdb-client" }, +] + [package.dev-dependencies] dev = [ + { name = "mypy" }, { name = "pytest" }, + { name = "pytest-cov" }, { name = "pytest-qt" }, + { name = "ruff" }, +] +docs = [ + { name = "linkify-it-py" }, + { name = "myst-parser" }, + { name = "nbsphinx" }, + { name = "pydata-sphinx-theme" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] [package.metadata] requires-dist = [ + { name = "influxdb-client", marker = "extra == 'monitoring'" }, + { name = "jsonschema" }, + { name = "numpy" }, + { name = "pandas" }, { name = "pyqt5" }, + { name = "pyyaml" }, { name = "pyzmq" }, { name = "qcodes" }, { name = "qtpy" }, + { name = "ruamel-yaml" }, { name = "scipy" }, ] +provides-extras = ["monitoring"] [package.metadata.requires-dev] dev = [ + { name = "mypy" }, { name = "pytest", specifier = ">=9.0.2" }, + { name = "pytest-cov" }, { name = "pytest-qt", specifier = ">=4.5.0" }, + { name = "ruff" }, +] +docs = [ + { name = "linkify-it-py" }, + { name = "myst-parser" }, + { name = "nbsphinx" }, + { name = "pydata-sphinx-theme" }, + { name = "sphinx" }, ] [[package]] @@ -562,9 +925,12 @@ name = "ipython" version = "9.11.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", @@ -627,6 +993,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, ] +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + [[package]] name = "jsonschema" version = "4.26.0" @@ -683,6 +1061,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, ] +[[package]] +name = "jupyterlab-pygments" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900, upload-time = "2023-11-23T09:26:37.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" }, +] + [[package]] name = "jupyterlab-widgets" version = "3.0.16" @@ -798,6 +1185,91 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/dd/8050c947d435c8d4bc94e3252f4d8bb8a76cfb424f043a8680be637a57f1/kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1", size = 73558, upload-time = "2026-03-09T13:15:52.112Z" }, ] +[[package]] +name = "librt" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/6b/3d5c13fb3e3c4f43206c8f9dfed13778c2ed4f000bacaa0b7ce3c402a265/librt-0.9.0.tar.gz", hash = "sha256:a0951822531e7aee6e0dfb556b30d5ee36bbe234faf60c20a16c01be3530869d", size = 184368, upload-time = "2026-04-09T16:06:26.173Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/1e/2ec7afcebcf3efea593d13aee18bbcfdd3a243043d848ebf385055e9f636/librt-0.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90904fac73c478f4b83f4ed96c99c8208b75e6f9a8a1910548f69a00f1eaa671", size = 67155, upload-time = "2026-04-09T16:04:42.933Z" }, + { url = "https://files.pythonhosted.org/packages/18/77/72b85afd4435268338ad4ec6231b3da8c77363f212a0227c1ff3b45e4d35/librt-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:789fff71757facc0738e8d89e3b84e4f0251c1c975e85e81b152cdaca927cc2d", size = 69916, upload-time = "2026-04-09T16:04:44.042Z" }, + { url = "https://files.pythonhosted.org/packages/27/fb/948ea0204fbe2e78add6d46b48330e58d39897e425560674aee302dca81c/librt-0.9.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1bf465d1e5b0a27713862441f6467b5ab76385f4ecf8f1f3a44f8aa3c695b4b6", size = 199635, upload-time = "2026-04-09T16:04:45.5Z" }, + { url = "https://files.pythonhosted.org/packages/ac/cd/894a29e251b296a27957856804cfd21e93c194aa131de8bb8032021be07e/librt-0.9.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f819e0c6413e259a17a7c0d49f97f405abadd3c2a316a3b46c6440b7dbbedbb1", size = 211051, upload-time = "2026-04-09T16:04:47.016Z" }, + { url = "https://files.pythonhosted.org/packages/18/8f/dcaed0bc084a35f3721ff2d081158db569d2c57ea07d35623ddaca5cfc8e/librt-0.9.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0785c2fb4a81e1aece366aa3e2e039f4a4d7d21aaaded5227d7f3c703427882", size = 224031, upload-time = "2026-04-09T16:04:48.207Z" }, + { url = "https://files.pythonhosted.org/packages/03/44/88f6c1ed1132cd418601cc041fbd92fed28b3a09f39de81978e0822d13ff/librt-0.9.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80b25c7b570a86c03b5da69e665809deb39265476e8e21d96a9328f9762f9990", size = 218069, upload-time = "2026-04-09T16:04:50.025Z" }, + { url = "https://files.pythonhosted.org/packages/a3/90/7d02e981c2db12188d82b4410ff3e35bfdb844b26aecd02233626f46af2b/librt-0.9.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d4d16b608a1c43d7e33142099a75cd93af482dadce0bf82421e91cad077157f4", size = 224857, upload-time = "2026-04-09T16:04:51.684Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c3/c77e706b7215ca32e928d47535cf13dbc3d25f096f84ddf8fbc06693e229/librt-0.9.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:194fc1a32e1e21fe809d38b5faea66cc65eaa00217c8901fbdb99866938adbdb", size = 219865, upload-time = "2026-04-09T16:04:52.949Z" }, + { url = "https://files.pythonhosted.org/packages/52/d1/32b0c1a0eb8461c70c11656c46a29f760b7c7edf3c36d6f102470c17170f/librt-0.9.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8c6bc1384d9738781cfd41d09ad7f6e8af13cfea2c75ece6bd6d2566cdea2076", size = 218451, upload-time = "2026-04-09T16:04:54.174Z" }, + { url = "https://files.pythonhosted.org/packages/74/d1/adfd0f9c44761b1d49b1bec66173389834c33ee2bd3c7fd2e2367f1942d4/librt-0.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:15cb151e52a044f06e54ac7f7b47adbfc89b5c8e2b63e1175a9d587c43e8942a", size = 241300, upload-time = "2026-04-09T16:04:55.452Z" }, + { url = "https://files.pythonhosted.org/packages/09/b0/9074b64407712f0003c27f5b1d7655d1438979155f049720e8a1abd9b1a1/librt-0.9.0-cp311-cp311-win32.whl", hash = "sha256:f100bfe2acf8a3689af9d0cc660d89f17286c9c795f9f18f7b62dd1a6b247ae6", size = 55668, upload-time = "2026-04-09T16:04:56.689Z" }, + { url = "https://files.pythonhosted.org/packages/24/19/40b77b77ce80b9389fb03971431b09b6b913911c38d412059e0b3e2a9ef2/librt-0.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:0b73e4266307e51c95e09c0750b7ec383c561d2e97d58e473f6f6a209952fbb8", size = 62976, upload-time = "2026-04-09T16:04:57.733Z" }, + { url = "https://files.pythonhosted.org/packages/70/9d/9fa7a64041e29035cb8c575af5f0e3840be1b97b4c4d9061e0713f171849/librt-0.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:bc5518873822d2faa8ebdd2c1a4d7c8ef47b01a058495ab7924cb65bdbf5fc9a", size = 53502, upload-time = "2026-04-09T16:04:58.806Z" }, + { url = "https://files.pythonhosted.org/packages/bf/90/89ddba8e1c20b0922783cd93ed8e64f34dc05ab59c38a9c7e313632e20ff/librt-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b3e3bc363f71bda1639a4ee593cb78f7fbfeacc73411ec0d4c92f00730010a4", size = 68332, upload-time = "2026-04-09T16:05:00.09Z" }, + { url = "https://files.pythonhosted.org/packages/a8/40/7aa4da1fb08bdeeb540cb07bfc8207cb32c5c41642f2594dbd0098a0662d/librt-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a09c2f5869649101738653a9b7ab70cf045a1105ac66cbb8f4055e61df78f2d", size = 70581, upload-time = "2026-04-09T16:05:01.213Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/73a2187e1031041e93b7e3a25aae37aa6f13b838c550f7e0f06f66766212/librt-0.9.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ca8e133d799c948db2ab1afc081c333a825b5540475164726dcbf73537e5c2f", size = 203984, upload-time = "2026-04-09T16:05:02.542Z" }, + { url = "https://files.pythonhosted.org/packages/5e/3d/23460d571e9cbddb405b017681df04c142fb1b04cbfce77c54b08e28b108/librt-0.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:603138ee838ee1583f1b960b62d5d0007845c5c423feb68e44648b1359014e27", size = 215762, upload-time = "2026-04-09T16:05:04.127Z" }, + { url = "https://files.pythonhosted.org/packages/de/1e/42dc7f8ab63e65b20640d058e63e97fd3e482c1edbda3570d813b4d0b927/librt-0.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4003f70c56a5addd6aa0897f200dd59afd3bf7bcd5b3cce46dd21f925743bc2", size = 230288, upload-time = "2026-04-09T16:05:05.883Z" }, + { url = "https://files.pythonhosted.org/packages/dc/08/ca812b6d8259ad9ece703397f8ad5c03af5b5fedfce64279693d3ce4087c/librt-0.9.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:78042f6facfd98ecb25e9829c7e37cce23363d9d7c83bc5f72702c5059eb082b", size = 224103, upload-time = "2026-04-09T16:05:07.148Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3f/620490fb2fa66ffd44e7f900254bc110ebec8dac6c1b7514d64662570e6f/librt-0.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a361c9434a64d70a7dbb771d1de302c0cc9f13c0bffe1cf7e642152814b35265", size = 232122, upload-time = "2026-04-09T16:05:08.386Z" }, + { url = "https://files.pythonhosted.org/packages/e9/83/12864700a1b6a8be458cf5d05db209b0d8e94ae281e7ec261dbe616597b4/librt-0.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:dd2c7e082b0b92e1baa4da28163a808672485617bc855cc22a2fd06978fa9084", size = 225045, upload-time = "2026-04-09T16:05:09.707Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1b/845d339c29dc7dbc87a2e992a1ba8d28d25d0e0372f9a0a2ecebde298186/librt-0.9.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7e6274fd33fc5b2a14d41c9119629d3ff395849d8bcbc80cf637d9e8d2034da8", size = 227372, upload-time = "2026-04-09T16:05:10.942Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fe/277985610269d926a64c606f761d58d3db67b956dbbf40024921e95e7fcb/librt-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5093043afb226ecfa1400120d1ebd4442b4f99977783e4f4f7248879009b227f", size = 248224, upload-time = "2026-04-09T16:05:12.254Z" }, + { url = "https://files.pythonhosted.org/packages/92/1b/ee486d244b8de6b8b5dbaefabe6bfdd4a72e08f6353edf7d16d27114da8d/librt-0.9.0-cp312-cp312-win32.whl", hash = "sha256:9edcc35d1cae9fd5320171b1a838c7da8a5c968af31e82ecc3dff30b4be0957f", size = 55986, upload-time = "2026-04-09T16:05:13.529Z" }, + { url = "https://files.pythonhosted.org/packages/89/7a/ba1737012308c17dc6d5516143b5dce9a2c7ba3474afd54e11f44a4d1ef3/librt-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc2917258e131ae5f958a4d872e07555b51cb7466a43433218061c74ef33745", size = 63260, upload-time = "2026-04-09T16:05:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/36/e4/01752c113da15127f18f7bf11142f5640038f062407a611c059d0036c6aa/librt-0.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:90e6d5420fc8a300518d4d2288154ff45005e920425c22cbbfe8330f3f754bd9", size = 53694, upload-time = "2026-04-09T16:05:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d7/1b3e26fffde1452d82f5666164858a81c26ebe808e7ae8c9c88628981540/librt-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29b68cd9714531672db62cc54f6e8ff981900f824d13fa0e00749189e13778e", size = 68367, upload-time = "2026-04-09T16:05:17.243Z" }, + { url = "https://files.pythonhosted.org/packages/a5/5b/c61b043ad2e091fbe1f2d35d14795e545d0b56b03edaa390fa1dcee3d160/librt-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d5c8a5929ac325729f6119802070b561f4db793dffc45e9ac750992a4ed4d22", size = 70595, upload-time = "2026-04-09T16:05:18.471Z" }, + { url = "https://files.pythonhosted.org/packages/a3/22/2448471196d8a73370aa2f23445455dc42712c21404081fcd7a03b9e0749/librt-0.9.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:756775d25ec8345b837ab52effee3ad2f3b2dfd6bbee3e3f029c517bd5d8f05a", size = 204354, upload-time = "2026-04-09T16:05:19.593Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5e/39fc4b153c78cfd2c8a2dcb32700f2d41d2312aa1050513183be4540930d/librt-0.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8f5d00b49818f4e2b1667db994488b045835e0ac16fe2f924f3871bd2b8ac5", size = 216238, upload-time = "2026-04-09T16:05:20.868Z" }, + { url = "https://files.pythonhosted.org/packages/d7/42/bc2d02d0fa7badfa63aa8d6dcd8793a9f7ef5a94396801684a51ed8d8287/librt-0.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c81aef782380f0f13ead670aae01825eb653b44b046aa0e5ebbb79f76ed4aa11", size = 230589, upload-time = "2026-04-09T16:05:22.305Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7b/e2d95cc513866373692aa5edf98080d5602dd07cabfb9e5d2f70df2f25f7/librt-0.9.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66b58fed90a545328e80d575467244de3741e088c1af928f0b489ebec3ef3858", size = 224610, upload-time = "2026-04-09T16:05:23.647Z" }, + { url = "https://files.pythonhosted.org/packages/31/d5/6cec4607e998eaba57564d06a1295c21b0a0c8de76e4e74d699e627bd98c/librt-0.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e78fb7419e07d98c2af4b8567b72b3eaf8cb05caad642e9963465569c8b2d87e", size = 232558, upload-time = "2026-04-09T16:05:25.025Z" }, + { url = "https://files.pythonhosted.org/packages/95/8c/27f1d8d3aaf079d3eb26439bf0b32f1482340c3552e324f7db9dca858671/librt-0.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c3786f0f4490a5cd87f1ed6cefae833ad6b1060d52044ce0434a2e85893afd0", size = 225521, upload-time = "2026-04-09T16:05:26.311Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d8/1e0d43b1c329b416017619469b3c3801a25a6a4ef4a1c68332aeaa6f72ca/librt-0.9.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8494cfc61e03542f2d381e71804990b3931175a29b9278fdb4a5459948778dc2", size = 227789, upload-time = "2026-04-09T16:05:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/2c/b4/d3d842e88610fcd4c8eec7067b0c23ef2d7d3bff31496eded6a83b0f99be/librt-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:07cf11f769831186eeac424376e6189f20ace4f7263e2134bdb9757340d84d4d", size = 248616, upload-time = "2026-04-09T16:05:29.181Z" }, + { url = "https://files.pythonhosted.org/packages/ec/28/527df8ad0d1eb6c8bdfa82fc190f1f7c4cca5a1b6d7b36aeabf95b52d74d/librt-0.9.0-cp313-cp313-win32.whl", hash = "sha256:850d6d03177e52700af605fd60db7f37dcb89782049a149674d1a9649c2138fd", size = 56039, upload-time = "2026-04-09T16:05:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a7/413652ad0d92273ee5e30c000fc494b361171177c83e57c060ecd3c21538/librt-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:a5af136bfba820d592f86c67affcef9b3ff4d4360ac3255e341e964489b48519", size = 63264, upload-time = "2026-04-09T16:05:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0a/92c244309b774e290ddb15e93363846ae7aa753d9586b8aad511c5e6145b/librt-0.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:4c4d0440a3a8e31d962340c3e1cc3fc9ee7febd34c8d8f770d06adb947779ea5", size = 53728, upload-time = "2026-04-09T16:05:33.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c1/184e539543f06ea2912f4b92a5ffaede4f9b392689e3f00acbf8134bee92/librt-0.9.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:3f05d145df35dca5056a8bc3838e940efebd893a54b3e19b2dda39ceaa299bcb", size = 67830, upload-time = "2026-04-09T16:05:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/23399bdcb7afca819acacdef31b37ee59de261bd66b503a7995c03c4b0dc/librt-0.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1c587494461ebd42229d0f1739f3aa34237dd9980623ecf1be8d3bcba79f4499", size = 70280, upload-time = "2026-04-09T16:05:35.649Z" }, + { url = "https://files.pythonhosted.org/packages/9f/0b/4542dc5a2b8772dbf92cafb9194701230157e73c14b017b6961a23598b03/librt-0.9.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0a2040f801406b93657a70b72fa12311063a319fee72ce98e1524da7200171f", size = 201925, upload-time = "2026-04-09T16:05:36.739Z" }, + { url = "https://files.pythonhosted.org/packages/31/d4/8ee7358b08fd0cfce051ef96695380f09b3c2c11b77c9bfbc367c921cce5/librt-0.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f38bc489037eca88d6ebefc9c4d41a4e07c8e8b4de5188a9e6d290273ad7ebb1", size = 212381, upload-time = "2026-04-09T16:05:38.043Z" }, + { url = "https://files.pythonhosted.org/packages/f2/94/a2025fe442abedf8b038038dab3dba942009ad42b38ea064a1a9e6094241/librt-0.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3fd278f5e6bf7c75ccd6d12344eb686cc020712683363b66f46ac79d37c799f", size = 227065, upload-time = "2026-04-09T16:05:39.394Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e9/b9fcf6afa909f957cfbbf918802f9dada1bd5d3c1da43d722fd6a310dc3f/librt-0.9.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fcbdf2a9ca24e87bbebb47f1fe34e531ef06f104f98c9ccfc953a3f3344c567a", size = 221333, upload-time = "2026-04-09T16:05:40.999Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7c/ba54cd6aa6a3c8cd12757a6870e0c79a64b1e6327f5248dcff98423f4d43/librt-0.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e306d956cfa027fe041585f02a1602c32bfa6bb8ebea4899d373383295a6c62f", size = 229051, upload-time = "2026-04-09T16:05:42.605Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4b/8cfdbad314c8677a0148bf0b70591d6d18587f9884d930276098a235461b/librt-0.9.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:465814ab157986acb9dfa5ccd7df944be5eefc0d08d31ec6e8d88bc71251d845", size = 222492, upload-time = "2026-04-09T16:05:43.842Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d1/2eda69563a1a88706808decdce035e4b32755dbfbb0d05e1a65db9547ed1/librt-0.9.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:703f4ae36d6240bfe24f542bac784c7e4194ec49c3ba5a994d02891649e2d85b", size = 223849, upload-time = "2026-04-09T16:05:45.054Z" }, + { url = "https://files.pythonhosted.org/packages/04/44/b2ed37df6be5b3d42cfe36318e0598e80843d5c6308dd63d0bf4e0ce5028/librt-0.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3be322a15ee5e70b93b7a59cfd074614f22cc8c9ff18bd27f474e79137ea8d3b", size = 245001, upload-time = "2026-04-09T16:05:46.34Z" }, + { url = "https://files.pythonhosted.org/packages/47/e7/617e412426df89169dd2a9ed0cc8752d5763336252c65dbf945199915119/librt-0.9.0-cp314-cp314-win32.whl", hash = "sha256:b8da9f8035bb417770b1e1610526d87ad4fc58a2804dc4d79c53f6d2cf5a6eb9", size = 51799, upload-time = "2026-04-09T16:05:47.738Z" }, + { url = "https://files.pythonhosted.org/packages/24/ed/c22ca4db0ca3cbc285e4d9206108746beda561a9792289c3c31281d7e9df/librt-0.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:b8bd70d5d816566a580d193326912f4a76ec2d28a97dc4cd4cc831c0af8e330e", size = 59165, upload-time = "2026-04-09T16:05:49.198Z" }, + { url = "https://files.pythonhosted.org/packages/24/56/875398fafa4cbc8f15b89366fc3287304ddd3314d861f182a4b87595ace0/librt-0.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:fc5758e2b7a56532dc33e3c544d78cbaa9ecf0a0f2a2da2df882c1d6b99a317f", size = 49292, upload-time = "2026-04-09T16:05:50.362Z" }, + { url = "https://files.pythonhosted.org/packages/4c/61/bc448ecbf9b2d69c5cff88fe41496b19ab2a1cbda0065e47d4d0d51c0867/librt-0.9.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f24b90b0e0c8cc9491fb1693ae91fe17cb7963153a1946395acdbdd5818429a4", size = 70175, upload-time = "2026-04-09T16:05:51.564Z" }, + { url = "https://files.pythonhosted.org/packages/60/f2/c47bb71069a73e2f04e70acbd196c1e5cc411578ac99039a224b98920fd4/librt-0.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fe56e80badb66fdcde06bef81bbaa5bfcf6fbd7aefb86222d9e369c38c6b228", size = 72951, upload-time = "2026-04-09T16:05:52.699Z" }, + { url = "https://files.pythonhosted.org/packages/29/19/0549df59060631732df758e8886d92088da5fdbedb35b80e4643664e8412/librt-0.9.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:527b5b820b47a09e09829051452bb0d1dd2122261254e2a6f674d12f1d793d54", size = 225864, upload-time = "2026-04-09T16:05:53.895Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f8/3b144396d302ac08e50f89e64452c38db84bc7b23f6c60479c5d3abd303c/librt-0.9.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d429bdd4ac0ab17c8e4a8af0ed2a7440b16eba474909ab357131018fe8c7e71", size = 241155, upload-time = "2026-04-09T16:05:55.191Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ce/ee67ec14581de4043e61d05786d2aed6c9b5338816b7859bcf07455c6a9f/librt-0.9.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7202bdcac47d3a708271c4304a474a8605a4a9a4a709e954bf2d3241140aa938", size = 252235, upload-time = "2026-04-09T16:05:56.549Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fa/0ead15daa2b293a54101550b08d4bafe387b7d4a9fc6d2b985602bae69b6/librt-0.9.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0d620e74897f8c2613b3c4e2e9c1e422eb46d2ddd07df540784d44117836af3", size = 244963, upload-time = "2026-04-09T16:05:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/29/68/9fbf9a9aa704ba87689e40017e720aced8d9a4d2b46b82451d8142f91ec9/librt-0.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d69fc39e627908f4c03297d5a88d9284b73f4d90b424461e32e8c2485e21c283", size = 257364, upload-time = "2026-04-09T16:05:59.686Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8d/9d60869f1b6716c762e45f66ed945b1e5dd649f7377684c3b176ae424648/librt-0.9.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c2640e23d2b7c98796f123ffd95cf2022c7777aa8a4a3b98b36c570d37e85eee", size = 247661, upload-time = "2026-04-09T16:06:00.938Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/a5c365093962310bfdb4f6af256f191085078ffb529b3f0cbebb5b33ebe2/librt-0.9.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:451daa98463b7695b0a30aa56bf637831ea559e7b8101ac2ef6382e8eb15e29c", size = 248238, upload-time = "2026-04-09T16:06:02.537Z" }, + { url = "https://files.pythonhosted.org/packages/a0/3c/2d34365177f412c9e19c0a29f969d70f5343f27634b76b765a54d8b27705/librt-0.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:928bd06eca2c2bbf4349e5b817f837509b0604342e65a502de1d50a7570afd15", size = 269457, upload-time = "2026-04-09T16:06:03.833Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cd/de45b239ea3bdf626f982a00c14bfcf2e12d261c510ba7db62c5969a27cd/librt-0.9.0-cp314-cp314t-win32.whl", hash = "sha256:a9c63e04d003bc0fb6a03b348018b9a3002f98268200e22cc80f146beac5dc40", size = 52453, upload-time = "2026-04-09T16:06:05.229Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f9/bfb32ae428aa75c0c533915622176f0a17d6da7b72b5a3c6363685914f70/librt-0.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f162af66a2ed3f7d1d161a82ca584efd15acd9c1cff190a373458c32f7d42118", size = 60044, upload-time = "2026-04-09T16:06:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/aa/47/7d70414bcdbb3bc1f458a8d10558f00bbfdb24e5a11740fc8197e12c3255/librt-0.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a4b25c6c25cac5d0d9d6d6da855195b254e0021e513e0249f0e3b444dc6e0e61", size = 50009, upload-time = "2026-04-09T16:06:07.995Z" }, +] + +[[package]] +name = "linkify-it-py" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "uc-micro-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" }, +] + [[package]] name = "locket" version = "1.0.0" @@ -807,6 +1279,92 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl", hash = "sha256:b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3", size = 4398, upload-time = "2022-04-20T22:04:42.23Z" }, ] +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + [[package]] name = "matplotlib" version = "3.10.8" @@ -883,6 +1441,186 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, ] +[[package]] +name = "mdit-py-plugins" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mistune" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/55/d01f0c4b45ade6536c51170b9043db8b2ec6ddf4a35c7ea3f5f559ac935b/mistune-3.2.0.tar.gz", hash = "sha256:708487c8a8cdd99c9d90eb3ed4c3ed961246ff78ac82f03418f5183ab70e398a", size = 95467, upload-time = "2025-12-23T11:36:34.994Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1", size = 53598, upload-time = "2025-12-23T11:36:33.211Z" }, +] + +[[package]] +name = "mypy" +version = "1.20.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/af/e3d4b3e9ec91a0ff9aabfdb38692952acf49bbb899c2e4c29acb3a6da3ae/mypy-1.20.2.tar.gz", hash = "sha256:e8222c26daaafd9e8626dec58ae36029f82585890589576f769a650dd20fd665", size = 3817349, upload-time = "2026-04-21T17:12:28.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/4d/9ebeae211caccbdaddde7ed5e31dfcf57faac66be9b11deb1dc6526c8078/mypy-1.20.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4077797a273e56e8843d001e9dfe4ba10e33323d6ade647ff260e5cd97d9758c", size = 14371307, upload-time = "2026-04-21T17:08:56.442Z" }, + { url = "https://files.pythonhosted.org/packages/95/d7/93473d34b61f04fac1aecc01368485c89c5c4af7a4b9a0cab5d77d04b63f/mypy-1.20.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cdecf62abcc4292500d7858aeae87a1f8f1150f4c4dd08fb0b336ee79b2a6df3", size = 13258917, upload-time = "2026-04-21T17:05:50.978Z" }, + { url = "https://files.pythonhosted.org/packages/e2/30/3dd903e8bafb7b5f7bf87fcd58f8382086dea2aa19f0a7b357f21f63071b/mypy-1.20.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c566c3a88b6ece59b3d70f65bedef17304f48eb52ff040a6a18214e1917b3254", size = 13700516, upload-time = "2026-04-21T17:11:33.161Z" }, + { url = "https://files.pythonhosted.org/packages/07/05/c61a140aba4c729ac7bc99ae26fc627c78a6e08f5b9dd319244ea71a3d7e/mypy-1.20.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0deb80d062b2479f2c87ae568f89845afc71d11bc41b04179e58165fd9f31e98", size = 14562889, upload-time = "2026-04-21T17:05:27.674Z" }, + { url = "https://files.pythonhosted.org/packages/fd/87/da78243742ffa8a36d98c3010f0d829f93d5da4e6786f1a1a6f2ad616502/mypy-1.20.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bba9ad231e92a3e424b3e56b65aa17704993425bba97e302c832f9466bb85bac", size = 14803844, upload-time = "2026-04-21T17:10:06.2Z" }, + { url = "https://files.pythonhosted.org/packages/37/52/10a1ddf91b40f843943a3c6db51e2df59c9e237f29d355e95eaab427461f/mypy-1.20.2-cp311-cp311-win_amd64.whl", hash = "sha256:baf593f2765fa3a6b1ef95807dbaa3d25b594f6a52adcc506a6b9cb115e1be67", size = 10846300, upload-time = "2026-04-21T17:12:23.886Z" }, + { url = "https://files.pythonhosted.org/packages/20/02/f9a4415b664c53bd34d6709be59da303abcae986dc4ac847b402edb6fa1e/mypy-1.20.2-cp311-cp311-win_arm64.whl", hash = "sha256:20175a1c0f49863946ec20b7f63255768058ac4f07d2b9ded6a6b46cfb5a9100", size = 9779498, upload-time = "2026-04-21T17:09:23.695Z" }, + { url = "https://files.pythonhosted.org/packages/71/4e/7560e4528db9e9b147e4c0f22660466bf30a0a1fe3d63d1b9d3b0fd354ee/mypy-1.20.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4dbfcf869f6b0517f70cf0030ba6ea1d6645e132337a7d5204a18d8d5636c02b", size = 14539393, upload-time = "2026-04-21T17:07:12.52Z" }, + { url = "https://files.pythonhosted.org/packages/32/d9/34a5efed8124f5a9234f55ac6a4ced4201e2c5b81e1109c49ad23190ec8c/mypy-1.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b6481b228d072315b053210b01ac320e1be243dc17f9e5887ef167f23f5fae4", size = 13361642, upload-time = "2026-04-21T17:06:53.742Z" }, + { url = "https://files.pythonhosted.org/packages/d1/14/eb377acf78c03c92d566a1510cda8137348215b5335085ef662ab82ecd3a/mypy-1.20.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34397cdced6b90b836e38182076049fdb41424322e0b0728c946b0939ebdf9f6", size = 13740347, upload-time = "2026-04-21T17:12:04.73Z" }, + { url = "https://files.pythonhosted.org/packages/b9/94/7e4634a32b641aa1c112422eed1bbece61ee16205f674190e8b536f884de/mypy-1.20.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5da6976f20cae27059ea8d0c86e7cef3de720e04c4bb9ee18e3690fdb792066", size = 14734042, upload-time = "2026-04-21T17:07:43.16Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f3/f7e62395cb7f434541b4491a01149a4439e28ace4c0c632bbf5431e92d1f/mypy-1.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56908d7e08318d39f85b1f0c6cfd47b0cac1a130da677630dac0de3e0623e102", size = 14964958, upload-time = "2026-04-21T17:11:00.665Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0d/47e3c3a0ec2a876e35aeac365df3cac7776c36bbd4ed18cc521e1b9d255b/mypy-1.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:d52ad8d78522da1d308789df651ee5379088e77c76cb1994858d40a426b343b9", size = 10911340, upload-time = "2026-04-21T17:10:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b2/6c852d72e0ea8b01f49da817fb52539993cde327e7d010e0103dc12d0dac/mypy-1.20.2-cp312-cp312-win_arm64.whl", hash = "sha256:785b08db19c9f214dc37d65f7c165d19a30fcecb48abfa30f31b01b5acaabb58", size = 9833947, upload-time = "2026-04-21T17:09:05.267Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c4/b93812d3a192c9bcf5df405bd2f30277cd0e48106a14d1023c7f6ed6e39b/mypy-1.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:edfbfca868cdd6bd8d974a60f8a3682f5565d3f5c99b327640cedd24c4264026", size = 14524670, upload-time = "2026-04-21T17:10:30.737Z" }, + { url = "https://files.pythonhosted.org/packages/f3/47/42c122501bff18eaf1e8f457f5c017933452d8acdc52918a9f59f6812955/mypy-1.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e2877a02380adfcdbc69071a0f74d6e9dbbf593c0dc9d174e1f223ffd5281943", size = 13336218, upload-time = "2026-04-21T17:08:44.069Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/75bbc92f41725fbd585fb17b440b1119b576105df1013622983e18640a93/mypy-1.20.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7488448de6007cd5177c6cea0517ac33b4c0f5ee9b5e9f2be51ce75511a85517", size = 13724906, upload-time = "2026-04-21T17:08:01.02Z" }, + { url = "https://files.pythonhosted.org/packages/a1/32/4c49da27a606167391ff0c39aa955707a00edc500572e562f7c36c08a71f/mypy-1.20.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb9c2fa06887e21d6a3a868762acb82aec34e2c6fd0174064f27c93ede68ad15", size = 14726046, upload-time = "2026-04-21T17:11:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/7f/fc/4e354a1bd70216359deb0c9c54847ee6b32ef78dfb09f5131ff99b494078/mypy-1.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d56a78b646f2e3daa865bc70cd5ec5a46c50045801ca8ff17a0c43abc97e3ee", size = 14955587, upload-time = "2026-04-21T17:12:16.033Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/c0f2056e9eb8f08c62cafd9715e4584b89132bdc832fcf85d27d07b5f3e5/mypy-1.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:2a4102b03bb7481d9a91a6da8d174740c9c8c4401024684b9ca3b7cc5e49852f", size = 10922681, upload-time = "2026-04-21T17:06:35.842Z" }, + { url = "https://files.pythonhosted.org/packages/e5/14/065e333721f05de8ef683d0aa804c23026bcc287446b61cac657b902ccac/mypy-1.20.2-cp313-cp313-win_arm64.whl", hash = "sha256:a95a9248b0c6fd933a442c03c3b113c3b61320086b88e2c444676d3fd1ca3330", size = 9830560, upload-time = "2026-04-21T17:07:51.023Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d1/b4ec96b0ecc620a4443570c6e95c867903428cfcde4206518eafdd5880c3/mypy-1.20.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:419413398fe250aae057fd2fe50166b61077083c9b82754c341cf4fd73038f30", size = 14524561, upload-time = "2026-04-21T17:06:27.325Z" }, + { url = "https://files.pythonhosted.org/packages/3a/63/d2c2ff4fa66bc49477d32dfa26e8a167ba803ea6a69c5efb416036909d30/mypy-1.20.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e73c07f23009962885c197ccb9b41356a30cc0e5a1d0c2ea8fd8fb1362d7f924", size = 13363883, upload-time = "2026-04-21T17:11:11.239Z" }, + { url = "https://files.pythonhosted.org/packages/2a/56/983916806bf4eddeaaa2c9230903c3669c6718552a921154e1c5182c701f/mypy-1.20.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c64e5973df366b747646fc98da921f9d6eba9716d57d1db94a83c026a08e0fb", size = 13742945, upload-time = "2026-04-21T17:08:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/19/65/0cd9285ab010ee8214c83d67c6b49417c40d86ce46f1aa109457b5a9b8d7/mypy-1.20.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a65aa591af023864fd08a97da9974e919452cfe19cb146c8a5dc692626445dc", size = 14706163, upload-time = "2026-04-21T17:05:15.51Z" }, + { url = "https://files.pythonhosted.org/packages/94/97/48ff3b297cafcc94d185243a9190836fb1b01c1b0918fff64e941e973cc9/mypy-1.20.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4fef51b01e638974a6e69885687e9bd40c8d1e09a6cd291cca0619625cf1f558", size = 14938677, upload-time = "2026-04-21T17:05:39.562Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a1/1b4233d255bdd0b38a1f284feeb1c143ca508c19184964e22f8d837ec851/mypy-1.20.2-cp314-cp314-win_amd64.whl", hash = "sha256:913485a03f1bcf5d279409a9d2b9ed565c151f61c09f29991e5faa14033da4c8", size = 11089322, upload-time = "2026-04-21T17:06:44.29Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/ce7ee2ba36aeb954ba50f18fa25d9c1188578654b97d02a66a15b6f09531/mypy-1.20.2-cp314-cp314-win_arm64.whl", hash = "sha256:c3bae4f855d965b5453784300c12ffc63a548304ac7f99e55d4dc7c898673aa3", size = 10017775, upload-time = "2026-04-21T17:07:20.732Z" }, + { url = "https://files.pythonhosted.org/packages/4e/a1/9d93a7d0b5859af0ead82b4888b46df6c8797e1bc5e1e262a08518c6d48e/mypy-1.20.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2de3dcea53babc1c3237a19002bc3d228ce1833278f093b8d619e06e7cc79609", size = 15549002, upload-time = "2026-04-21T17:08:23.107Z" }, + { url = "https://files.pythonhosted.org/packages/00/d2/09a6a10ee1bf0008f6c144d9676f2ca6a12512151b4e0ad0ff6c4fac5337/mypy-1.20.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:52b176444e2e5054dfcbcb8c75b0b719865c96247b37407184bbfca5c353f2c2", size = 14401942, upload-time = "2026-04-21T17:07:31.837Z" }, + { url = "https://files.pythonhosted.org/packages/57/da/9594b75c3c019e805250bed3583bdf4443ff9e6ef08f97e39ae308cb06f2/mypy-1.20.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:688c3312e5dadb573a2c69c82af3a298d43ecf9e6d264e0f95df960b5f6ac19c", size = 15041649, upload-time = "2026-04-21T17:09:34.653Z" }, + { url = "https://files.pythonhosted.org/packages/97/77/f75a65c278e6e8eba2071f7f5a90481891053ecc39878cc444634d892abe/mypy-1.20.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29752dbbf8cc53f89f6ac096d363314333045c257c9c75cbd189ca2de0455744", size = 15864588, upload-time = "2026-04-21T17:11:44.936Z" }, + { url = "https://files.pythonhosted.org/packages/d7/46/1a4e1c66e96c1a3246ddf5403d122ac9b0a8d2b7e65730b9d6533ba7a6d3/mypy-1.20.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:803203d2b6ea644982c644895c2f78b28d0e208bba7b27d9b921e0ec5eb207c6", size = 16093956, upload-time = "2026-04-21T17:10:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/5a/2c/78a8851264dec38cd736ca5b8bc9380674df0dd0be7792f538916157716c/mypy-1.20.2-cp314-cp314t-win_amd64.whl", hash = "sha256:9bcb8aa397ff0093c824182fd76a935a9ba7ad097fcbef80ae89bf6c1731d8ec", size = 12568661, upload-time = "2026-04-21T17:11:54.473Z" }, + { url = "https://files.pythonhosted.org/packages/83/01/cd7318aa03493322ce275a0e14f4f52b8896335e4e79d4fb8153a7ad2b77/mypy-1.20.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e061b58443f1736f8a37c48978d7ab581636d6ab03e3d4f99e3fa90463bb9382", size = 10389240, upload-time = "2026-04-21T17:09:42.719Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/f23c163e25b11074188251b0b5a0342625fc1cdb6af604757174fa9acc9b/mypy-1.20.2-py3-none-any.whl", hash = "sha256:a94c5a76ab46c5e6257c7972b6c8cff0574201ca7dc05647e33e795d78680563", size = 2637314, upload-time = "2026-04-21T17:05:54.5Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "myst-parser" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "jinja2" }, + { name = "markdown-it-py" }, + { name = "mdit-py-plugins" }, + { name = "pyyaml" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/fa/7b45eef11b7971f0beb29d27b7bfe0d747d063aa29e170d9edd004733c8a/myst_parser-5.0.0.tar.gz", hash = "sha256:f6f231452c56e8baa662cc352c548158f6a16fcbd6e3800fc594978002b94f3a", size = 98535, upload-time = "2026-01-15T09:08:18.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/ac/686789b9145413f1a61878c407210e41bfdb097976864e0913078b24098c/myst_parser-5.0.0-py3-none-any.whl", hash = "sha256:ab31e516024918296e169139072b81592336f2fef55b8986aa31c9f04b5f7211", size = 84533, upload-time = "2026-01-15T09:08:16.788Z" }, +] + +[[package]] +name = "nbclient" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "nbformat" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/91/1c1d5a4b9a9ebba2b4e32b8c852c2975c872aec1fe42ab5e516b2cecd193/nbclient-0.10.4.tar.gz", hash = "sha256:1e54091b16e6da39e297b0ece3e10f6f29f4ac4e8ee515d29f8a7099bd6553c9", size = 62554, upload-time = "2025-12-23T07:45:46.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl", hash = "sha256:9162df5a7373d70d606527300a95a975a47c137776cd942e52d9c7e29ff83440", size = 25465, upload-time = "2025-12-23T07:45:44.51Z" }, +] + +[[package]] +name = "nbconvert" +version = "7.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "bleach", extra = ["css"] }, + { name = "defusedxml" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyterlab-pygments" }, + { name = "markupsafe" }, + { name = "mistune" }, + { name = "nbclient" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "pandocfilters" }, + { name = "pygments" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/b1/708e53fe2e429c103c6e6e159106bcf0357ac41aa4c28772bd8402339051/nbconvert-7.17.1.tar.gz", hash = "sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2", size = 865311, upload-time = "2026-04-08T00:44:14.914Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl", hash = "sha256:aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8", size = 261927, upload-time = "2026-04-08T00:44:12.845Z" }, +] + +[[package]] +name = "nbformat" +version = "5.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastjsonschema" }, + { name = "jsonschema" }, + { name = "jupyter-core" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" }, +] + +[[package]] +name = "nbsphinx" +version = "0.9.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "jinja2" }, + { name = "nbconvert" }, + { name = "nbformat" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/d1/82081750f8a78ad0399c6ed831d42623b891904e8e7b8a75878225cf1dce/nbsphinx-0.9.8.tar.gz", hash = "sha256:d0765908399a8ee2b57be7ae881cf2ea58d66db3af7bbf33e6eb48f83bea5495", size = 417469, upload-time = "2025-11-28T17:41:02.336Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/78/843bcf0cf31f88d2f8a9a063d2d80817b1901657d83d65b89b3aa835732e/nbsphinx-0.9.8-py3-none-any.whl", hash = "sha256:92d95ee91784e56bc633b60b767a6b6f23a0445f891e24641ce3c3f004759ccf", size = 31961, upload-time = "2025-11-28T17:41:00.796Z" }, +] + [[package]] name = "nest-asyncio" version = "1.6.0" @@ -1062,6 +1800,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/68/b0/34937815889fa982613775e4b97fddd13250f11012d769949c5465af2150/pandas-3.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:108dd1790337a494aa80e38def654ca3f0968cf4f362c85f44c15e471667102d", size = 9452085, upload-time = "2026-02-17T22:20:14.331Z" }, ] +[[package]] +name = "pandocfilters" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454, upload-time = "2024-01-18T20:08:13.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663, upload-time = "2024-01-18T20:08:11.28Z" }, +] + [[package]] name = "parso" version = "0.8.6" @@ -1084,6 +1831,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl", hash = "sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f", size = 18905, upload-time = "2024-05-06T19:51:39.271Z" }, ] +[[package]] +name = "pathspec" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/17/9c3094b822982b9f1ea666d8580ce59000f61f87c1663556fb72031ad9ec/pathspec-1.1.0.tar.gz", hash = "sha256:f5d7c555da02fd8dde3e4a2354b6aba817a89112fa8f333f7917a2a4834dd080", size = 133918, upload-time = "2026-04-23T01:46:22.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/c9/8eed0486f074e9f1ca7f8ce5ad663e65f12fdab344028d658fa1b03d35e0/pathspec-1.1.0-py3-none-any.whl", hash = "sha256:574b128f7456bd899045ccd142dd446af7e6cfd0072d63ad73fbc55fbb4aaa42", size = 56264, upload-time = "2026-04-23T01:46:20.606Z" }, +] + [[package]] name = "pexpect" version = "4.9.0" @@ -1318,6 +2074,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] +[[package]] +name = "pydata-sphinx-theme" +version = "0.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "accessible-pygments" }, + { name = "babel" }, + { name = "beautifulsoup4" }, + { name = "docutils" }, + { name = "pygments" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/f7/c74c7100a7f4c0f77b5dcacb7dfdb8fee774fb70e487dd97acba2b930774/pydata_sphinx_theme-0.17.1.tar.gz", hash = "sha256:2cfc1d926c753c77039b7ee53f0ccebcbee5e81f0db61432b01cbb10ad7fd0af", size = 4991415, upload-time = "2026-04-21T13:00:34.263Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/bc/2cb8c78300ce1ace4eeac3b3522218cea2c2053bfa6b4e32cc972a477f9a/pydata_sphinx_theme-0.17.1-py3-none-any.whl", hash = "sha256:320b022d7808bdf5920d9a28e573f27aace9b23e1af6ca103eecc752411df492", size = 6823346, upload-time = "2026-04-21T13:00:31.978Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -1407,6 +2182,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + [[package]] name = "pytest-qt" version = "4.5.0" @@ -1606,6 +2395,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/69/76/37c0ccd5ab968a6a438f9c623aeecc84c202ab2fabc6a8fd927580c15b5a/QtPy-2.4.3-py3-none-any.whl", hash = "sha256:72095afe13673e017946cc258b8d5da43314197b741ed2890e563cf384b51aa1", size = 95045, upload-time = "2025-02-11T15:09:24.162Z" }, ] +[[package]] +name = "reactivex" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/af/38a4b62468e4c5bd50acf511d86fe62e65a466aa6abb55b1d59a4a9e57f3/reactivex-4.1.0.tar.gz", hash = "sha256:c7499e3c802bccaa20839b3e17355a7d939573fded3f38ba3d4796278a169a3d", size = 113482, upload-time = "2025-11-05T21:44:24.557Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/9e/3c2f5d3abb6c5d82f7696e1e3c69b7279049e928596ce82ed25ca97a08f3/reactivex-4.1.0-py3-none-any.whl", hash = "sha256:485750ec8d9b34bcc8ff4318971d234dc4f595058a1b4435a74aefef4b2bc9bd", size = 218588, upload-time = "2025-11-05T21:44:23.015Z" }, +] + [[package]] name = "referencing" version = "0.37.0" @@ -1620,6 +2421,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, ] +[[package]] +name = "requests" +version = "2.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, +] + +[[package]] +name = "roman-numerals" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, +] + [[package]] name = "rpds-py" version = "0.30.0" @@ -1737,6 +2562,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" }, ] +[[package]] +name = "ruff" +version = "0.15.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/192f3d7103816158dfd5ea50d098ef2aec19194e6cbccd4b3485bdb2eb2d/ruff-0.15.11.tar.gz", hash = "sha256:f092b21708bf0e7437ce9ada249dfe688ff9a0954fc94abab05dcea7dcd29c33", size = 4637264, upload-time = "2026-04-16T18:46:26.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/1e/6aca3427f751295ab011828e15e9bf452200ac74484f1db4be0197b8170b/ruff-0.15.11-py3-none-linux_armv6l.whl", hash = "sha256:e927cfff503135c558eb581a0c9792264aae9507904eb27809cdcff2f2c847b7", size = 10607943, upload-time = "2026-04-16T18:46:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/e7/26/1341c262e74f36d4e84f3d6f4df0ac68cd53331a66bfc5080daa17c84c0b/ruff-0.15.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7a1b5b2938d8f890b76084d4fa843604d787a912541eae85fd7e233398bbb73e", size = 10988592, upload-time = "2026-04-16T18:46:00.742Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/850b1d6ffa9564fbb6740429bad53df1094082fe515c8c1e74b6d8d05f18/ruff-0.15.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d4176f3d194afbdaee6e41b9ccb1a2c287dba8700047df474abfbe773825d1cb", size = 10338501, upload-time = "2026-04-16T18:46:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/f2/11/cc1284d3e298c45a817a6aadb6c3e1d70b45c9b36d8d9cce3387b495a03a/ruff-0.15.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b17c886fb88203ced3afe7f14e8d5ae96e9d2f4ccc0ee66aa19f2c2675a27e4", size = 10670693, upload-time = "2026-04-16T18:46:41.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9e/f8288b034ab72b371513c13f9a41d9ba3effac54e24bfb467b007daee2ca/ruff-0.15.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49fafa220220afe7758a487b048de4c8f9f767f37dfefad46b9dd06759d003eb", size = 10416177, upload-time = "2026-04-16T18:46:21.717Z" }, + { url = "https://files.pythonhosted.org/packages/85/71/504d79abfd3d92532ba6bbe3d1c19fada03e494332a59e37c7c2dabae427/ruff-0.15.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2ab8427e74a00d93b8bda1307b1e60970d40f304af38bccb218e056c220120d", size = 11221886, upload-time = "2026-04-16T18:46:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/43/5a/947e6ab7a5ad603d65b474be15a4cbc6d29832db5d762cd142e4e3a74164/ruff-0.15.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:195072c0c8e1fc8f940652073df082e37a5d9cb43b4ab1e4d0566ab8977a13b7", size = 12075183, upload-time = "2026-04-16T18:46:07.944Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a1/0b7bb6268775fdd3a0818aee8efd8f5b4e231d24dd4d528ced2534023182/ruff-0.15.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a0996d486af3920dec930a2e7daed4847dfc12649b537a9335585ada163e9e", size = 11516575, upload-time = "2026-04-16T18:46:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/30/c3/bb5168fc4d233cc06e95f482770d0f3c87945a0cd9f614b90ea8dc2f2833/ruff-0.15.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bef2cb556d509259f1fe440bb9cd33c756222cf0a7afe90d15edf0866702431", size = 11306537, upload-time = "2026-04-16T18:46:36.988Z" }, + { url = "https://files.pythonhosted.org/packages/e4/92/4cfae6441f3967317946f3b788136eecf093729b94d6561f963ed810c82e/ruff-0.15.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:030d921a836d7d4a12cf6e8d984a88b66094ccb0e0f17ddd55067c331191bf19", size = 11296813, upload-time = "2026-04-16T18:46:24.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/26/972784c5dde8313acde8ac71ba8ac65475b85db4a2352a76c9934361f9bc/ruff-0.15.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e783b599b4577788dbbb66b9addcef87e9a8832f4ce0c19e34bf55543a2f890", size = 10633136, upload-time = "2026-04-16T18:46:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/5b/53/3985a4f185020c2f367f2e08a103032e12564829742a1b417980ce1514a0/ruff-0.15.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ae90592246625ba4a34349d68ec28d4400d75182b71baa196ddb9f82db025ef5", size = 10424701, upload-time = "2026-04-16T18:46:10.381Z" }, + { url = "https://files.pythonhosted.org/packages/d3/57/bf0dfb32241b56c83bb663a826133da4bf17f682ba8c096973065f6e6a68/ruff-0.15.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1f111d62e3c983ed20e0ca2e800f8d77433a5b1161947df99a5c2a3fb60514f0", size = 10873887, upload-time = "2026-04-16T18:46:29.157Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/e48076b2a57dc33ee8c7a957296f97c744ca891a8ffb4ffb1aaa3b3f517d/ruff-0.15.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:06f483d6646f59eaffba9ae30956370d3a886625f511a3108994000480621d1c", size = 11404316, upload-time = "2026-04-16T18:46:19.462Z" }, + { url = "https://files.pythonhosted.org/packages/88/27/0195d15fe7a897cbcba0904792c4b7c9fdd958456c3a17d2ea6093716a9a/ruff-0.15.11-py3-none-win32.whl", hash = "sha256:476a2aa56b7da0b73a3ee80b6b2f0e19cce544245479adde7baa65466664d5f3", size = 10655535, upload-time = "2026-04-16T18:46:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5e/c927b325bd4c1d3620211a4b96f47864633199feed60fa936025ab27e090/ruff-0.15.11-py3-none-win_amd64.whl", hash = "sha256:8b6756d88d7e234fb0c98c91511aae3cd519d5e3ed271cae31b20f39cb2a12a3", size = 11779692, upload-time = "2026-04-16T18:46:17.268Z" }, + { url = "https://files.pythonhosted.org/packages/63/b6/aeadee5443e49baa2facd51131159fd6301cc4ccfc1541e4df7b021c37dd/ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4", size = 11032614, upload-time = "2026-04-16T18:46:34.487Z" }, +] + [[package]] name = "schema" version = "0.7.8" @@ -1826,6 +2676,150 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "snowballstemmer" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, +] + +[[package]] +name = "sphinx" +version = "9.0.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version < '3.12' and sys_platform == 'emscripten'", + "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "alabaster", marker = "python_full_version < '3.12'" }, + { name = "babel", marker = "python_full_version < '3.12'" }, + { name = "colorama", marker = "python_full_version < '3.12' and sys_platform == 'win32'" }, + { name = "docutils", marker = "python_full_version < '3.12'" }, + { name = "imagesize", marker = "python_full_version < '3.12'" }, + { name = "jinja2", marker = "python_full_version < '3.12'" }, + { name = "packaging", marker = "python_full_version < '3.12'" }, + { name = "pygments", marker = "python_full_version < '3.12'" }, + { name = "requests", marker = "python_full_version < '3.12'" }, + { name = "roman-numerals", marker = "python_full_version < '3.12'" }, + { name = "snowballstemmer", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/3f/4bbd76424c393caead2e1eb89777f575dee5c8653e2d4b6afd7a564f5974/sphinx-9.0.4-py3-none-any.whl", hash = "sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb", size = 3917713, upload-time = "2025-12-04T07:45:24.944Z" }, +] + +[[package]] +name = "sphinx" +version = "9.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "alabaster", marker = "python_full_version >= '3.12'" }, + { name = "babel", marker = "python_full_version >= '3.12'" }, + { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "docutils", marker = "python_full_version >= '3.12'" }, + { name = "imagesize", marker = "python_full_version >= '3.12'" }, + { name = "jinja2", marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.12'" }, + { name = "pygments", marker = "python_full_version >= '3.12'" }, + { name = "requests", marker = "python_full_version >= '3.12'" }, + { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, + { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + [[package]] name = "stack-data" version = "0.6.3" @@ -1849,6 +2843,72 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" }, ] +[[package]] +name = "tinycss2" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085, upload-time = "2024-10-24T14:58:29.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + [[package]] name = "toolz" version = "1.1.0" @@ -1914,6 +2974,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, ] +[[package]] +name = "uc-micro-py" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611, upload-time = "2026-03-01T06:31:27.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" }, +] + [[package]] name = "uncertainties" version = "3.2.3" @@ -1923,6 +2992,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl", hash = "sha256:313353900d8f88b283c9bad81e7d2b2d3d4bcc330cbace35403faaed7e78890a", size = 60118, upload-time = "2025-04-21T19:58:26.864Z" }, ] +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + [[package]] name = "versioningit" version = "3.3.0" @@ -1944,6 +3022,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, ] +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, +] + [[package]] name = "websockets" version = "16.0" From 38717fb3c7194e69e0b511f94b3f6fd2dca35472 Mon Sep 17 00:00:00 2001 From: marcosf2 Date: Fri, 24 Apr 2026 12:17:12 -0300 Subject: [PATCH 05/12] First pass at styler --- src/instrumentserver/__init__.py | 6 +- src/instrumentserver/apps.py | 102 +++-- src/instrumentserver/base.py | 13 +- src/instrumentserver/blueprints.py | 291 +++++++------ src/instrumentserver/client/__init__.py | 1 - src/instrumentserver/client/application.py | 94 +++-- src/instrumentserver/client/core.py | 25 +- src/instrumentserver/client/proxy.py | 383 +++++++++++------- src/instrumentserver/config.py | 90 ++-- src/instrumentserver/gui/__init__.py | 12 +- src/instrumentserver/gui/base_instrument.py | 197 +++++---- src/instrumentserver/gui/instruments.py | 275 ++++++++----- src/instrumentserver/gui/misc.py | 50 ++- src/instrumentserver/gui/parameters.py | 75 ++-- src/instrumentserver/helpers.py | 40 +- src/instrumentserver/log.py | 62 +-- src/instrumentserver/monitoring/listener.py | 85 ++-- src/instrumentserver/params.py | 118 +++--- src/instrumentserver/resource.py | 13 +- src/instrumentserver/serialize.py | 115 +++--- src/instrumentserver/server/application.py | 341 ++++++++++------ src/instrumentserver/server/core.py | 304 ++++++++------ src/instrumentserver/server/pollingWorker.py | 12 +- .../testing/create_instrument.py | 11 +- .../testing/dummy_instruments/generic.py | 200 ++++----- .../testing/dummy_instruments/rf.py | 189 ++++++--- test/client_workingdir/init_client.py | 26 +- test/notebooks/Autoupdate.ipynb | 44 +- .../Prototype the ParamManager.ipynb | 34 +- ...n the station server from a notebook.ipynb | 12 +- test/prototyping/server_admin.py | 29 +- test/prototyping/testing_parameter_manager.py | 13 +- test/pytest/conftest.py | 16 +- test/pytest/test_base.py | 51 ++- test/pytest/test_basic_functionality.py | 28 +- test/pytest/test_client_station.py | 70 ++-- test/pytest/test_config.py | 162 +++++--- test/pytest/test_helpers.py | 47 ++- test/pytest/test_json_serializable.py | 80 ++-- test/pytest/test_param_manager.py | 121 +++--- test/pytest/test_serialize.py | 3 +- test/pytest/test_server_gui.py | 50 ++- .../test_async_requests/client_station_gui.py | 13 +- test/test_async_requests/demo_concurrency.py | 16 +- test/test_async_requests/test_client.py | 25 +- 45 files changed, 2398 insertions(+), 1546 deletions(-) diff --git a/src/instrumentserver/__init__.py b/src/instrumentserver/__init__.py index 482657e..a41991c 100644 --- a/src/instrumentserver/__init__.py +++ b/src/instrumentserver/__init__.py @@ -20,8 +20,7 @@ def getInstrumentserverPath(*subfolder: str) -> str: return os.path.join(path, *subfolder) -PARAMS_SCHEMA_PATH = os.path.join(getInstrumentserverPath('schemas'), - 'parameters.json') +PARAMS_SCHEMA_PATH = os.path.join(getInstrumentserverPath("schemas"), "parameters.json") DEFAULT_PORT = 5555 @@ -31,4 +30,5 @@ def getInstrumentserverPath(*subfolder: str) -> str: from .log import setupLogging, logger from .client import Client -InstrumentClient = Client \ No newline at end of file + +InstrumentClient = Client diff --git a/src/instrumentserver/apps.py b/src/instrumentserver/apps.py index 8610a73..a830703 100644 --- a/src/instrumentserver/apps.py +++ b/src/instrumentserver/apps.py @@ -19,9 +19,8 @@ from instrumentserver.server.application import DetachedServerGui -setupLogging(addStreamHandler=True, - logFile=os.path.abspath('instrumentserver.log')) -logger = logging.getLogger('instrumentserver') +setupLogging(addStreamHandler=True, logFile=os.path.abspath("instrumentserver.log")) +logger = logging.getLogger("instrumentserver") logger.setLevel(logging.INFO) @@ -43,24 +42,38 @@ def serverWithGui(**kwargs): def serverScript() -> None: - parser = argparse.ArgumentParser(description='Starting the instrumentserver') + parser = argparse.ArgumentParser(description="Starting the instrumentserver") parser.add_argument("-p", "--port", default=5555) parser.add_argument("--gui", default=True) parser.add_argument("--allow_user_shutdown", default=False) - parser.add_argument("-a", "--listen_at", type=str, nargs="*", - help="On which network addresses we listen.") - parser.add_argument("-i", "--init_script", default='', - type=str) - parser.add_argument("-c", "--config", type=str, default='') + parser.add_argument( + "-a", + "--listen_at", + type=str, + nargs="*", + help="On which network addresses we listen.", + ) + parser.add_argument("-i", "--init_script", default="", type=str) + parser.add_argument("-c", "--config", type=str, default="") args = parser.parse_args() # Load and process the config file if any. configPath = args.config - stationConfig, serverConfig, guiConfig, tempFile, pollingRates, pollingThread, ipAddresses = None, None, None, None, None, None, None - if configPath != '': + ( + stationConfig, + serverConfig, + guiConfig, + tempFile, + pollingRates, + pollingThread, + ipAddresses, + ) = None, None, None, None, None, None, None + if configPath != "": # Separates the corresponding settings into the 5 necessary parts - stationConfig, serverConfig, guiConfig, tempFile, pollingRates, ipAddresses = loadConfig(configPath) + stationConfig, serverConfig, guiConfig, tempFile, pollingRates, ipAddresses = ( + loadConfig(configPath) + ) if pollingRates is not None and pollingRates != {}: pollingThread = QtCore.QThread() pollWorker = PollingWorker(pollingRates=pollingRates) @@ -68,25 +81,29 @@ def serverScript() -> None: pollingThread.started.connect(pollWorker.run) pollingThread.start() - if args.gui == 'False': - server(port=args.port, - allowUserShutdown=args.allow_user_shutdown, - addresses=args.listen_at, - initScript=args.init_script, - serverConfig=serverConfig, - stationConfig=stationConfig, - guiConfig=guiConfig, - pollingThread=pollingThread, - ipAddresses=ipAddresses) + if args.gui == "False": + server( + port=args.port, + allowUserShutdown=args.allow_user_shutdown, + addresses=args.listen_at, + initScript=args.init_script, + serverConfig=serverConfig, + stationConfig=stationConfig, + guiConfig=guiConfig, + pollingThread=pollingThread, + ipAddresses=ipAddresses, + ) else: - serverWithGui(port=args.port, - addresses=args.listen_at, - initScript=args.init_script, - serverConfig=serverConfig, - stationConfig=stationConfig, - guiConfig=guiConfig, - pollingThread=pollingThread, - ipAddresses=ipAddresses) + serverWithGui( + port=args.port, + addresses=args.listen_at, + initScript=args.init_script, + serverConfig=serverConfig, + stationConfig=stationConfig, + guiConfig=guiConfig, + pollingThread=pollingThread, + ipAddresses=ipAddresses, + ) # Close and delete the temporary files if tempFile is not None: @@ -96,7 +113,9 @@ def serverScript() -> None: def parameterManagerScript() -> None: - parser = argparse.ArgumentParser(description='Starting a parameter manager instrument GUI') + parser = argparse.ArgumentParser( + description="Starting a parameter manager instrument GUI" + ) parser.add_argument("--name", default="parameter_manager") parser.add_argument("--port", default=5555) args = parser.parse_args() @@ -110,17 +129,20 @@ def parameterManagerScript() -> None: pm = cli.get_instrument(args.name) else: pm = cli.find_or_create_instrument( - args.name, 'instrumentserver.params.ParameterManager') + args.name, "instrumentserver.params.ParameterManager" + ) pm.fromFile() pm.update() - _ = widgetMainWindow(ParameterManagerGui(pm), 'Parameter Manager') + _ = widgetMainWindow(ParameterManagerGui(pm), "Parameter Manager") app.exec_() def detachedServerScript() -> None: - parser = argparse.ArgumentParser(description='Starting a detached instance of the GUI for the server') + parser = argparse.ArgumentParser( + description="Starting a detached instance of the GUI for the server" + ) parser.add_argument("--host", default="localhost") parser.add_argument("--port", default=5555) args = parser.parse_args() @@ -132,10 +154,16 @@ def detachedServerScript() -> None: def clientStationScript() -> None: - parser = argparse.ArgumentParser(description='Starting a client station GUI') + parser = argparse.ArgumentParser(description="Starting a client station GUI") parser.add_argument("--host", default="localhost", help="Server host address") parser.add_argument("--port", default=5555, type=int, help="Server port") - parser.add_argument("-c", "--config", type=str, default='', help="Path to client station config file (YAML)") + parser.add_argument( + "-c", + "--config", + type=str, + default="", + help="Path to client station config file (YAML)", + ) args = parser.parse_args() app = QtWidgets.QApplication([]) @@ -145,5 +173,3 @@ def clientStationScript() -> None: window = ClientStationGui(station) window.show() app.exec_() - - diff --git a/src/instrumentserver/base.py b/src/instrumentserver/base.py index 5b489b0..1204630 100644 --- a/src/instrumentserver/base.py +++ b/src/instrumentserver/base.py @@ -6,6 +6,7 @@ logger = logging.getLogger(__name__) + def encode(data): return json.dumps(to_dict(data)) @@ -19,7 +20,7 @@ def send(socket, data, use_string=True): if use_string: return socket.send_string(payload) else: - return socket.send(payload.encode('utf-8')) + return socket.send(payload.encode("utf-8")) def recv(socket): @@ -30,7 +31,7 @@ def recv(socket): logger.warning(f"Additional part found in recv: {leftover}") if len(parts) == 1: data = parts[0] - elif len(parts) == 2 and parts[0] == b'': # optional empty delimiter + elif len(parts) == 2 and parts[0] == b"": # optional empty delimiter data = parts[1] else: data = parts[-1] # assume last part is the actual message @@ -40,15 +41,15 @@ def recv(socket): def send_router(socket, identity, message): socket.setsockopt(zmq.SNDTIMEO, 5000) socket.setsockopt(zmq.LINGER, 0) - payload = encode(message).encode('utf-8') - socket.send_multipart([identity, b'', payload]) + payload = encode(message).encode("utf-8") + socket.send_multipart([identity, b"", payload]) def recv_router(socket): parts = socket.recv_multipart() if len(parts) == 2: identity, payload = parts - elif len(parts) == 3 and parts[1] == b'': + elif len(parts) == 3 and parts[1] == b"": identity, payload = parts[0], parts[2] else: raise ValueError(f"Malformed ROUTER message: {parts}") @@ -65,7 +66,7 @@ def sendBroadcast(socket, name, message): :param messages: The data to send. """ socket.send_string(name, flags=zmq.SNDMORE) - socket.send(encode(message).encode('utf-8')) + socket.send(encode(message).encode("utf-8")) def recvMultipart(socket): diff --git a/src/instrumentserver/blueprints.py b/src/instrumentserver/blueprints.py index 8411841..7ad2bc2 100644 --- a/src/instrumentserver/blueprints.py +++ b/src/instrumentserver/blueprints.py @@ -60,7 +60,12 @@ import numpy as np from qcodes import ( - Station, Instrument, InstrumentChannel, Parameter, ParameterWithSetpoints) + Station, + Instrument, + InstrumentChannel, + Parameter, + ParameterWithSetpoints, +) from qcodes.instrument.base import InstrumentBase from qcodes.utils.validators import Validator @@ -68,14 +73,10 @@ logger = logging.getLogger(__name__) -INSTRUMENT_MODULE_BASE_CLASSES = [ - Instrument, InstrumentChannel, InstrumentBase -] +INSTRUMENT_MODULE_BASE_CLASSES = [Instrument, InstrumentChannel, InstrumentBase] InstrumentModuleType = Union[Instrument, InstrumentChannel, InstrumentBase] -PARAMETER_BASE_CLASSES = [ - Parameter, ParameterWithSetpoints -] +PARAMETER_BASE_CLASSES = [Parameter, ParameterWithSetpoints] ParameterType = Union[Parameter, ParameterWithSetpoints] @@ -83,16 +84,17 @@ @dataclass class ParameterBluePrint: """Spec necessary for creating parameter proxies.""" + name: str path: str base_class: str parameter_class: str gettable: bool = True settable: bool = True - unit: str = '' - docstring: str = '' + unit: str = "" + docstring: str = "" setpoints: Optional[List[str]] = None - _class_type: str = 'ParameterBluePrint' + _class_type: str = "ParameterBluePrint" def __repr__(self) -> str: return str(self) @@ -101,7 +103,7 @@ def __str__(self) -> str: return f"{self.name}: {self.parameter_class}" def tostr(self, indent=0): - i = indent * ' ' + i = indent * " " ret = f"""{self.name}: {self.parameter_class} {i}- unit: {self.unit} {i}- path: {self.path} @@ -116,16 +118,19 @@ def toJson(self): return bluePrintToDict(self) -def bluePrintFromParameter(path: str, param: ParameterType) -> \ - Union[ParameterBluePrint, None]: +def bluePrintFromParameter( + path: str, param: ParameterType +) -> Union[ParameterBluePrint, None]: base_class = None for bc in PARAMETER_BASE_CLASSES: if isinstance(param, bc): base_class = bc break if base_class is None: - logger.warning(f"Blueprints for parameter base type of {param} are " - f"currently not supported.") + logger.warning( + f"Blueprints for parameter base type of {param} are " + f"currently not supported." + ) return None bp = ParameterBluePrint( @@ -133,12 +138,12 @@ def bluePrintFromParameter(path: str, param: ParameterType) -> \ path=path, base_class=typeClassPath(base_class), parameter_class=objectClassPath(param), - gettable=True if hasattr(param, 'get') else False, - settable=True if hasattr(param, 'set') else False, + gettable=True if hasattr(param, "get") else False, + settable=True if hasattr(param, "set") else False, unit=param.unit, docstring=param.__doc__ or "", ) - if hasattr(param, 'setpoints'): + if hasattr(param, "setpoints"): bp.setpoints = [setpoint.name for setpoint in param.setpoints] return bp @@ -147,12 +152,13 @@ def bluePrintFromParameter(path: str, param: ParameterType) -> \ @dataclass class MethodBluePrint: """Spec necessary for creating method proxies""" + name: str path: str call_signature_str: str signature_parameters: dict docstring: str = "" - _class_type: str = 'MethodBluePrint' + _class_type: str = "MethodBluePrint" def __repr__(self): return str(self) @@ -161,7 +167,7 @@ def __str__(self): return f"{self.name}{str(self.call_signature_str)}" def tostr(self, indent=0): - i = indent * ' ' + i = indent * " " ret = f"""{self.name}{str(self.call_signature_str)} {i}- path: {self.path} """ @@ -169,7 +175,9 @@ def tostr(self, indent=0): # we might want to be careful to keep them in the correct order @classmethod - def signature_str_and_params_from_obj(cls, sig: inspect.Signature) -> Tuple[str, dict]: + def signature_str_and_params_from_obj( + cls, sig: inspect.Signature + ) -> Tuple[str, dict]: call_signature_str = str(sig) param_dict = {} for name, param in sig.parameters.items(): @@ -184,7 +192,7 @@ def bluePrintFromMethod(path: str, method: Callable) -> Union[MethodBluePrint, N sig = inspect.signature(method) sig_str, param_dict = MethodBluePrint.signature_str_and_params_from_obj(sig) bp = MethodBluePrint( - name=path.split('.')[-1], + name=path.split(".")[-1], path=path, call_signature_str=sig_str, signature_parameters=param_dict, @@ -196,25 +204,31 @@ def bluePrintFromMethod(path: str, method: Callable) -> Union[MethodBluePrint, N @dataclass class InstrumentModuleBluePrint: """Spec necessary for creating instrument proxies.""" + name: str path: str base_class: str instrument_module_class: str - docstring: str = '' + docstring: str = "" parameters: Optional[Dict[str, ParameterBluePrint]] = field(default_factory=dict) methods: Optional[Dict[str, MethodBluePrint]] = field(default_factory=dict) - submodules: Optional[Dict[str, "InstrumentModuleBluePrint"]] = field(default_factory=dict) - _class_type: str = 'InstrumentModuleBluePrint' - - def __init__(self, name: str, - path: str, - base_class: str, - instrument_module_class: str, - docstring: str = '', - parameters: Optional[Dict[str, ParameterBluePrint]] = None, - methods: Optional[Dict[str, MethodBluePrint]] = None, - submodules: Optional[Dict[str, "InstrumentModuleBluePrint"]] = None, - _class_type: str = 'InstrumentModuleBluePrint'): + submodules: Optional[Dict[str, "InstrumentModuleBluePrint"]] = field( + default_factory=dict + ) + _class_type: str = "InstrumentModuleBluePrint" + + def __init__( + self, + name: str, + path: str, + base_class: str, + instrument_module_class: str, + docstring: str = "", + parameters: Optional[Dict[str, ParameterBluePrint]] = None, + methods: Optional[Dict[str, MethodBluePrint]] = None, + submodules: Optional[Dict[str, "InstrumentModuleBluePrint"]] = None, + _class_type: str = "InstrumentModuleBluePrint", + ): self.name = name self.path = path @@ -255,7 +269,7 @@ def __init__(self, name: str, else: raise AttributeError("parameters has invalid type.") - self._class_type = 'InstrumentModuleBluePrint' + self._class_type = "InstrumentModuleBluePrint" def __repr__(self) -> str: return str(self) @@ -264,7 +278,7 @@ def __str__(self) -> str: return f"{self.name}: {self.instrument_module_class}" def tostr(self, indent=0): - i = indent * ' ' + i = indent * " " ret = f"""{i}{self.name}: {self.instrument_module_class} {i}- path: {self.path} {i}- base class: {self.base_class} @@ -287,16 +301,18 @@ def toJson(self): return bluePrintToDict(self) -def bluePrintFromInstrumentModule(path: str, ins: InstrumentModuleType) -> \ - Union[InstrumentModuleBluePrint, None]: +def bluePrintFromInstrumentModule( + path: str, ins: InstrumentModuleType +) -> Union[InstrumentModuleBluePrint, None]: base_class = None for bc in INSTRUMENT_MODULE_BASE_CLASSES: if isinstance(ins, bc): base_class = bc break if base_class is None: - logger.warning(f"Blueprints for instrument base type of {ins} are " - f"currently not supported.") + logger.warning( + f"Blueprints for instrument base type of {ins} are currently not supported." + ) return None bp = InstrumentModuleBluePrint( @@ -320,7 +336,7 @@ def bluePrintFromInstrumentModule(path: str, ins: InstrumentModuleType) -> \ for elt in dir(ins): # don't include private methods, or methods that belong to the qcodes # base classes. - if elt[0] == '_' or hasattr(base_class, elt): + if elt[0] == "_" or hasattr(base_class, elt): continue o = getattr(ins, elt) if callable(o) and not isinstance(o, tuple(PARAMETER_BASE_CLASSES)): @@ -342,19 +358,20 @@ def bluePrintFromInstrumentModule(path: str, ins: InstrumentModuleType) -> \ @dataclass class ParameterBroadcastBluePrint: """Blueprint to broadcast parameter changes.""" + name: str action: str value: int | None = None unit: str = "" - _class_type: str = 'ParameterBroadcastBluePrint' + _class_type: str = "ParameterBroadcastBluePrint" def __str__(self) -> str: ret = f"""\"name\":\"{self.name}\": {{ \"action\":\"{self.action}" """ if self.value is not None: - ret = ret + f"\n \"value\":\"{self.value}\"" + ret = ret + f'\n "value":"{self.value}"' if self.unit is not None: - ret = ret + f"\n \"unit\":\"{self.unit}\"" + ret = ret + f'\n "unit":"{self.unit}"' ret = ret + f"""\n}}""" return ret @@ -363,7 +380,7 @@ def __repr__(self): def pprint(self, indent=0): - i = indent * ' ' + i = indent * " " ret = f"""name: {self.name} {i}- action: {self.action} {i}- value: {self.value} @@ -376,7 +393,12 @@ def toJson(self): return bluePrintToDict(self) -BluePrintType = Union[ParameterBluePrint, MethodBluePrint, InstrumentModuleBluePrint, ParameterBroadcastBluePrint] +BluePrintType = Union[ + ParameterBluePrint, + MethodBluePrint, + InstrumentModuleBluePrint, + ParameterBroadcastBluePrint, +] def _dictToJson(_dict: dict, json_type: bool = True) -> dict: @@ -408,7 +430,9 @@ def bluePrintToDict(bp: BluePrintType, json_type=True) -> dict: if isinstance(value, get_args(BluePrintType)): bp_dict[my_field.name] = bluePrintToDict(value, json_type) elif isinstance(value, dict): - bp_dict[my_field.name] = _dictToJson(bp.__getattribute__(my_field.name), json_type) + bp_dict[my_field.name] = _dictToJson( + bp.__getattribute__(my_field.name), json_type + ) else: if json_type: bp_dict[my_field.name] = str(bp.__getattribute__(my_field.name)) @@ -422,25 +446,25 @@ class Operation(Enum): """Valid operations for the server.""" #: Get a list of instruments the server has instantiated. - get_existing_instruments = 'get_existing_instruments' + get_existing_instruments = "get_existing_instruments" #: Create a new instrument. - create_instrument = 'create_instrument' + create_instrument = "create_instrument" #: Get the blueprint of an object. - get_blueprint = 'get_blueprint' + get_blueprint = "get_blueprint" #: Make a call to an object. - call = 'call' + call = "call" #: Get the station contents as parameter dict. - get_param_dict = 'get_param_dict' + get_param_dict = "get_param_dict" #: Set station parameters from a dictionary. - set_params = 'set_params' + set_params = "set_params" #: Gets the GUI configuration for an instrument. - get_gui_config = 'get_gui_config' + get_gui_config = "get_gui_config" @dataclass @@ -452,7 +476,7 @@ class InstrumentCreationSpec: #: Name of the new instrument, I separate this from args and kwargs to # make it easier to be found. - name: str = '' + name: str = "" #: Arguments to pass to the constructor. args: Optional[Tuple] = None @@ -460,12 +484,12 @@ class InstrumentCreationSpec: #: kw args to pass to the constructor. kwargs: Optional[Dict[str, Any]] = None - _class_type: str = 'InstrumentCreationSpec' + _class_type: str = "InstrumentCreationSpec" def toJson(self): ret = asdict(self) - ret['args'] = iterable_to_serialized_dict(self.args) - ret['kwargs'] = dict_to_serialized_dict(self.kwargs) + ret["args"] = iterable_to_serialized_dict(self.args) + ret["kwargs"] = dict_to_serialized_dict(self.kwargs) return ret @@ -483,12 +507,12 @@ class CallSpec: #: kw args to pass. kwargs: Optional[Dict[str, Any]] = None - _class_type: str = 'CallSpec' + _class_type: str = "CallSpec" def toJson(self): ret = asdict(self) - ret['args'] = iterable_to_serialized_dict(self.args) - ret['kwargs'] = dict_to_serialized_dict(self.kwargs) + ret["args"] = iterable_to_serialized_dict(self.args) + ret["kwargs"] = dict_to_serialized_dict(self.kwargs) return ret @@ -498,7 +522,7 @@ class ParameterSerializeSpec: path: Optional[str] = None #: Which attributes to include for each parameter. Default is ['values']. - attrs: List[str] = field(default_factory=lambda: ['values']) + attrs: List[str] = field(default_factory=lambda: ["values"]) #: Additional arguments to pass to the serialization function #: :func:`.serialize.toParamDict`. @@ -508,12 +532,12 @@ class ParameterSerializeSpec: #: :func:`.serialize.toParamDict`. kwargs: Optional[Dict[str, Any]] = field(default_factory=dict) - _class_type: str = 'ParameterSerializeSpec' + _class_type: str = "ParameterSerializeSpec" def toJson(self): ret = asdict(self) - ret['args'] = iterable_to_serialized_dict(self.args) - ret['kwargs'] = dict_to_serialized_dict(self.kwargs) + ret["args"] = iterable_to_serialized_dict(self.args) + ret["kwargs"] = dict_to_serialized_dict(self.kwargs) return ret @@ -578,48 +602,48 @@ class ServerInstruction: #: Generic keyword arguments. kwargs: Optional[Dict[str, Any]] = field(default_factory=dict) - _class_type: str = 'ServerInstruction' + _class_type: str = "ServerInstruction" def validate(self): if self.operation is Operation.create_instrument: if not isinstance(self.create_instrument_spec, InstrumentCreationSpec): - raise ValueError('Invalid instrument creation spec.') + raise ValueError("Invalid instrument creation spec.") if self.operation is Operation.call: if not isinstance(self.call_spec, CallSpec): - raise ValueError('Invalid call spec.') + raise ValueError("Invalid call spec.") if self.operation is Operation.get_gui_config: if not isinstance(self.requested_path, str): - raise ValueError('Invalid requested path.') + raise ValueError("Invalid requested path.") def toJson(self): - ret = {'operation': str(self.operation.name)} + ret = {"operation": str(self.operation.name)} if self.create_instrument_spec is None: - ret['create_instrument_spec'] = None + ret["create_instrument_spec"] = None else: - ret['create_instrument_spec'] = self.create_instrument_spec.toJson() + ret["create_instrument_spec"] = self.create_instrument_spec.toJson() if self.call_spec is None: - ret['call_spec'] = None + ret["call_spec"] = None else: - ret['call_spec'] = self.call_spec.toJson() + ret["call_spec"] = self.call_spec.toJson() if self.requested_path is None: - ret['requested_path'] = None + ret["requested_path"] = None else: - ret['requested_path'] = str(self.requested_path) + ret["requested_path"] = str(self.requested_path) if self.serialization_opts is None: - ret['serialization_opts'] = None + ret["serialization_opts"] = None else: - ret['serialization_opts'] = self.serialization_opts.toJson() + ret["serialization_opts"] = self.serialization_opts.toJson() - ret['set_parameters'] = self.set_parameters - ret['args'] = iterable_to_serialized_dict(self.args) - ret['kwargs'] = dict_to_serialized_dict(self.kwargs) - ret['_class_type'] = self._class_type + ret["set_parameters"] = self.set_parameters + ret["args"] = iterable_to_serialized_dict(self.args) + ret["kwargs"] = dict_to_serialized_dict(self.kwargs) + ret["_class_type"] = self._class_type return ret @@ -635,6 +659,7 @@ class ServerResponse: If an error occurs, `message` is typically ``None``, and `error` contains an error message or object describing the error. """ + #: The return message. message: Optional[Any] = None @@ -642,11 +667,14 @@ class ServerResponse: error: Optional[Union[None, str, Warning, Exception]] = None #: The type of the class, used for deserializing it. - _class_type: str = 'ServerResponse' - - def __init__(self, message: Optional[Any] = None, - error: Optional[Union[None, str, Warning, Exception, dict]] = None, - _class_type: str = 'ServerResponse'): + _class_type: str = "ServerResponse" + + def __init__( + self, + message: Optional[Any] = None, + error: Optional[Union[None, str, Warning, Exception, dict]] = None, + _class_type: str = "ServerResponse", + ): self.message = message if isinstance(message, str): try: @@ -662,35 +690,39 @@ def __init__(self, message: Optional[Any] = None, after_json_loads = json.loads(message) self.message = after_json_loads except json.JSONDecodeError as e: - logger.debug(f'message could not be decoded by JSON and will be treated as a string: {message}') + logger.debug( + f"message could not be decoded by JSON and will be treated as a string: {message}" + ) if isinstance(error, dict): - self.error = Exception(error['message']) + self.error = Exception(error["message"]) else: self.error = error - self._class_type = 'ServerResponse' + self._class_type = "ServerResponse" def toJson(self): ret = {} if isinstance(self.message, get_args(BluePrintType)): - ret['message'] = self.message.toJson() - elif hasattr(self.message, 'attributes'): - ret['message'] = _convert_arbitrary_obj_to_dict(self.message) + ret["message"] = self.message.toJson() + elif hasattr(self.message, "attributes"): + ret["message"] = _convert_arbitrary_obj_to_dict(self.message) elif not isinstance(self.message, str) and isinstance(self.message, Iterable): if isinstance(self.message, dict): message_dict = dict_to_serialized_dict(self.message) - ret['message'] = str(message_dict) + ret["message"] = str(message_dict) else: message_iterable = iterable_to_serialized_dict(self.message) - ret['message'] = str(message_iterable) + ret["message"] = str(message_iterable) else: - ret['message'] = str(self.message) + ret["message"] = str(self.message) if isinstance(self.error, Exception): - ret['error'] = dict(exception_type=str(type(self.error)), message=str(self.error)) + ret["error"] = dict( + exception_type=str(type(self.error)), message=str(self.error) + ) else: - ret['error'] = str(self.error) + ret["error"] = str(self.error) - ret['_class_type'] = self._class_type + ret["_class_type"] = self._class_type return ret @@ -702,13 +734,13 @@ def _convert_arbitrary_obj_to_dict(obj: object) -> Dict[str, Any]: all of those attributes are natively JSON serializable. These should also be accepted as keyword arguments in the constructor of the object. """ - if not hasattr(obj, 'attributes'): + if not hasattr(obj, "attributes"): raise AttributeError('Object does not have an attribute called "attributes"') obj_dict = {} for attr in obj.attributes: obj_dict[attr] = getattr(obj, attr) - obj_dict['_class_type'] = f'{obj.__module__}.{obj.__class__.__name__}' + obj_dict["_class_type"] = f"{obj.__module__}.{obj.__class__.__name__}" return obj_dict @@ -719,22 +751,22 @@ def _convert_dict_to_obj(item_dict: dict) -> Any: Assumes that the dictionary has a key '_class_type' indicating what class it should be instantiated from. """ - class_type = item_dict['_class_type'] + class_type = item_dict["_class_type"] # if a dot is present indicates the class is arbitrary and needs to be imported - if '.' in class_type: - parts = class_type.split('.') - mod = importlib.import_module('.'.join(parts[:-1])) + if "." in class_type: + parts = class_type.split(".") + mod = importlib.import_module(".".join(parts[:-1])) cls = getattr(mod, parts[-1]) - item_dict.pop('_class_type') + item_dict.pop("_class_type") return cls(**item_dict) try: - instantiated_obj = eval(f'{class_type}(**item_dict)') + instantiated_obj = eval(f"{class_type}(**item_dict)") # built-ins (like complex) will not want the _class_type argument except TypeError: - cls = item_dict.pop('_class_type') - instantiated_obj = eval(f'{cls}(**item_dict)') + cls = item_dict.pop("_class_type") + instantiated_obj = eval(f"{cls}(**item_dict)") return instantiated_obj @@ -766,19 +798,23 @@ def iterable_to_serialized_dict(iterable: Optional[Iterable[Any]] = None): serialized_iterable = iterable_to_serialized_dict(iterable=item) converted_iterable.append(serialized_iterable) - elif hasattr(item, 'attributes'): + elif hasattr(item, "attributes"): arg_dict = _convert_arbitrary_obj_to_dict(item) converted_iterable.append(arg_dict) elif isinstance(item, complex): - arg_dict = dict(real=float(item.real), imag=float(item.imag), _class_type='complex') + arg_dict = dict( + real=float(item.real), imag=float(item.imag), _class_type="complex" + ) converted_iterable.append(arg_dict) else: converted_iterable.append(str(item)) if isinstance(iterable, np.ndarray): - converted_iterable = dict(object=converted_iterable, _class_type="numpy.array") + converted_iterable = dict( + object=converted_iterable, _class_type="numpy.array" + ) return converted_iterable @@ -802,11 +838,15 @@ def dict_to_serialized_dict(dct: Optional[Dict[str, Any]] = None): serialized_iterable = iterable_to_serialized_dict(iterable=value) converted_dict[name] = serialized_iterable - elif hasattr(value, 'attributes'): + elif hasattr(value, "attributes"): kwarg_dict = _convert_arbitrary_obj_to_dict(value) converted_dict[name] = kwarg_dict elif isinstance(value, complex): - kwarg_dict = dict(real=float(value.real), imag=float(value.imag), _class_type='complex') + kwarg_dict = dict( + real=float(value.real), + imag=float(value.imag), + _class_type="complex", + ) converted_dict[name] = kwarg_dict else: converted_dict[name] = str(value) @@ -830,7 +870,7 @@ def _is_numeric(val) -> Optional[Union[float, complex]]: Tries to convert the input into a int or a float. If it can, returns the conversion. Otherwise returns None. """ try: - if val is not None and not '.' in val: + if val is not None and not "." in val: int_conversion = int(val) return int_conversion except Exception: @@ -857,14 +897,14 @@ def deserialize_obj(data: Any): that dictionary represents a serialized object that needs to be instantiated. The function will try and deserailize any other item in the dictionary. """ - if data is None or data == 'None': + if data is None or data == "None": return None elif isinstance(data, dict): deserialized_dict = {} for key, value in data.items(): deserialized_dict[key] = deserialize_obj(value) - if '_class_type' in deserialized_dict: + if "_class_type" in deserialized_dict: obj_instance = _convert_dict_to_obj(deserialized_dict) return obj_instance @@ -873,24 +913,26 @@ def deserialize_obj(data: Any): numeric_form = _is_numeric(data) if numeric_form is not None: return numeric_form - elif data == 'True': + elif data == "True": return True - elif data == 'False': + elif data == "False": return False - elif data == '{}': + elif data == "{}": return {} - elif data == '[]': + elif data == "[]": return [] elif isinstance(data, str): if len(data) > 0: # Try and load other items in the string it since it might be a nested item - if (data[0] == '{' and data[-1] == '}') or (data[0] == '[' and data[-1] == ']'): + if (data[0] == "{" and data[-1] == "}") or ( + data[0] == "[" and data[-1] == "]" + ): try: loaded_json = json.loads(data.replace("'", '"')) return deserialize_obj(loaded_json) except json.JSONDecodeError as e: - logger.debug('str could not be decoded, treating it as a str') + logger.debug("str could not be decoded, treating it as a str") return data @@ -900,4 +942,3 @@ def deserialize_obj(data: Any): deserialized_iterable.append(deserialize_obj(item)) # Returns the same type of iterable return deserialized_iterable - diff --git a/src/instrumentserver/client/__init__.py b/src/instrumentserver/client/__init__.py index 6213417..b135c9c 100644 --- a/src/instrumentserver/client/__init__.py +++ b/src/instrumentserver/client/__init__.py @@ -1,3 +1,2 @@ from .core import sendRequest from .proxy import ProxyInstrument, Client, QtClient, SubClient, ClientStation - diff --git a/src/instrumentserver/client/application.py b/src/instrumentserver/client/application.py index e4d1bdf..4e134f7 100644 --- a/src/instrumentserver/client/application.py +++ b/src/instrumentserver/client/application.py @@ -28,7 +28,7 @@ class ServerWidget(QtWidgets.QWidget): - def __init__(self, client_station:ClientStation, parent=None): + def __init__(self, client_station: ClientStation, parent=None): super().__init__(parent) self.client_station = client_station @@ -58,7 +58,9 @@ def __init__(self, client_station:ClientStation, parent=None): lh = self.cmd.fontMetrics().lineSpacing() self.cmd.setFixedHeight(lh * rows + 2 * self.cmd.frameWidth() + 8) # Let it grow horizontally - self.cmd.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) + self.cmd.setSizePolicy( + QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed + ) form_layout.addRow("Host:", self.host) form_layout.addRow("Port:", self.port) @@ -87,10 +89,9 @@ def _tint_readonly(self, le, bg="#f3f6fa"): le.setPalette(pal) # def restart_server(self): - # todo: to be implemented, ssh to server pc and start the server there. - # need to close the port if occupied. - # print(self.cmd.toPlainText()) - + # todo: to be implemented, ssh to server pc and start the server there. + # need to close the port if occupied. + # print(self.cmd.toPlainText()) class ClientStationGui(QtWidgets.QMainWindow): @@ -106,14 +107,23 @@ def __init__(self, station: ClientStation): # Set unique Windows App ID so that this app can have separate taskbar entry than other Qt apps if sys.platform == "win32": import ctypes - ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("InstrumentServer.ClientStation") - self.setWindowIcon(QtGui.QIcon(getInstrumentserverPath("resource","icons")+"/client_app_icon.svg")) + + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( + "InstrumentServer.ClientStation" + ) + self.setWindowIcon( + QtGui.QIcon( + getInstrumentserverPath("resource", "icons") + "/client_app_icon.svg" + ) + ) self.station = station self.cli = station.client # set up the listener thread and worker that listens to update messages emitted by the server (from all clients) self.listenerThread = QtCore.QThread() - self.listener = SubClient(instruments=None, sub_host=self.cli.host, sub_port=self.cli.port+1) + self.listener = SubClient( + instruments=None, sub_host=self.cli.host, sub_port=self.cli.port + 1 + ) self.listener.moveToThread(self.listenerThread) self.listenerThread.started.connect(self.listener.connect) self.listener.finished.connect(self.listenerThread.quit) @@ -131,8 +141,8 @@ def __init__(self, station: ClientStation): self.tabs.currentChanged.connect(self.onTabChanged) # --- client station - self.stationList = StationList() # instrument list - self.stationObjInfo = StationObjectInfo() # instrument docs + self.stationList = StationList() # instrument list + self.stationObjInfo = StationObjectInfo() # instrument docs for inst in self.station.instruments.values(): self.stationList.addInstrument(inst.bp) @@ -145,18 +155,17 @@ def __init__(self, station: ClientStation): stationWidgets.addWidget(self.stationObjInfo) stationWidgets.setSizes([200, 500]) - self.tabs.addUnclosableTab(stationWidgets, 'Station') + self.tabs.addUnclosableTab(stationWidgets, "Station") self.addParameterLoadSaveToolbar() # --- log widget self.log_widget = LogWidget(level=logging.INFO) - self.tabs.addUnclosableTab(self.log_widget, 'Log') + self.tabs.addUnclosableTab(self.log_widget, "Log") # --- server widget self.server_widget = ServerWidget(self.station) - self.tabs.addUnclosableTab(self.server_widget, 'Server') - + self.tabs.addUnclosableTab(self.server_widget, "Server") # adjust window size screen_geometry = QGuiApplication.primaryScreen().availableGeometry() @@ -166,7 +175,7 @@ def __init__(self, station: ClientStation): @QtCore.Slot(ParameterBroadcastBluePrint) def listenerEvent(self, message: ParameterBroadcastBluePrint): - if message.action == 'parameter-update': + if message.action == "parameter-update": logger.info(f"{message.action}: {message.name}: {message.value}") def openInstrumentTab(self, item: QtWidgets.QListWidgetItem, index: int): @@ -181,25 +190,32 @@ def openInstrumentTab(self, item: QtWidgets.QListWidgetItem, index: int): # Get GUI config from station config (patterns already merged by config.py) instrument_config = self.station.full_config.get(name, {}) - gui_config = instrument_config.get('gui', {}) - gui_kwargs = gui_config.get('kwargs', {}) + gui_config = instrument_config.get("gui", {}) + gui_kwargs = gui_config.get("kwargs", {}) widgetClass = GenericInstrument # Check if a custom GUI type is specified - if 'type' in gui_config: + if "type" in gui_config: try: # import the widget - moduleName = '.'.join(gui_config['type'].split('.')[:-1]) - widgetClassName = gui_config['type'].split('.')[-1] + moduleName = ".".join(gui_config["type"].split(".")[:-1]) + widgetClassName = gui_config["type"].split(".")[-1] module = importlib.import_module(moduleName) widgetClass = getattr(module, widgetClassName) except (ImportError, AttributeError) as e: - logger.warning(f"Failed to load custom GUI '{gui_config['type']}' for '{name}': {e}. Using default GenericInstrument.") + logger.warning( + f"Failed to load custom GUI '{gui_config['type']}' for '{name}': {e}. Using default GenericInstrument." + ) widgetClass = GenericInstrument - ins_widget = widgetClass(instrument, parent=self, sub_host=self.cli.host, sub_port=self.cli.port+1, - **gui_kwargs) + ins_widget = widgetClass( + instrument, + parent=self, + sub_host=self.cli.host, + sub_port=self.cli.port + 1, + **gui_kwargs, + ) # add tab ins_widget.setObjectName(name) @@ -223,7 +239,9 @@ def onTabChanged(self, index): widget = self.tabs.widget(index) # if instrument tab is not in 'instrumentTabsOpen' yet, tab must be just open, in this case the constructor # of the parameter widget should have already called refresh, so we don't have to do that again. - if hasattr(widget, "parametersList") and (widget.objectName() in self.instrumentTabsOpen): + if hasattr(widget, "parametersList") and ( + widget.objectName() in self.instrumentTabsOpen + ): widget.parametersList.model.refreshAll() @QtCore.Slot(str) @@ -248,7 +266,7 @@ def addParameterLoadSaveToolbar(self): self.paramPathEdit = QtWidgets.QLineEdit(pathWidget) self.paramPathEdit.setPlaceholderText("Parameter file path") - self.paramPathEdit.setText(str(Path.cwd()/"client_params.json")) + self.paramPathEdit.setText(str(Path.cwd() / "client_params.json")) self.paramPathEdit.setClearButtonEnabled(True) self.paramPathEdit.setMinimumWidth(280) h = self.paramPathEdit.fontMetrics().height() + 10 @@ -260,7 +278,7 @@ def addParameterLoadSaveToolbar(self): pathLayout.addWidget(lbl) pathLayout.addWidget(self.paramPathEdit, 1) # stretch - + pathAction = QtWidgets.QWidgetAction(self.toolBar) pathAction.setDefaultWidget(pathWidget) self.toolBar.addAction(pathAction) @@ -272,7 +290,7 @@ def addParameterLoadSaveToolbar(self): saveAct = QtWidgets.QAction(QtGui.QIcon(":/icons/save.svg"), "Save", self) loadAct.triggered.connect(self.loadParams) saveAct.triggered.connect(self.saveParams) - + self.toolBar.addAction(browseBtn) self.toolBar.addAction(loadAct) self.toolBar.addAction(saveAct) @@ -292,7 +310,9 @@ def browseParamPath(self): def saveParams(self): file_path = self.paramPathEdit.text() if not file_path: - QtWidgets.QMessageBox.warning(self, "No file path", "Please specify a path to save parameters.") + QtWidgets.QMessageBox.warning( + self, "No file path", "Please specify a path to save parameters." + ) return try: self.station.save_parameters(file_path) @@ -304,7 +324,9 @@ def saveParams(self): def loadParams(self): file_path = self.paramPathEdit.text() if not file_path: - QtWidgets.QMessageBox.warning(self, "No file path", "Please specify a path to load parameters.") + QtWidgets.QMessageBox.warning( + self, "No file path", "Please specify a path to load parameters." + ) return try: self.station.load_parameters(file_path) @@ -313,20 +335,23 @@ def loadParams(self): # Refresh all tabs for i in range(self.tabs.count()): widget = self.tabs.widget(i) - if hasattr(widget, 'parametersList') and hasattr(widget.parametersList, 'model'): + if hasattr(widget, "parametersList") and hasattr( + widget.parametersList, "model" + ): widget.parametersList.model.refreshAll() except Exception as e: QtWidgets.QMessageBox.critical(self, "Load Error", str(e)) - @QtCore.Slot(str) - def closeInstrument(self, name: str):#, item: QtWidgets.QListWidgetItem): + def closeInstrument(self, name: str): # , item: QtWidgets.QListWidgetItem): try: # close instrument on server self.station.close_instrument(name) except Exception as e: - QtWidgets.QMessageBox.critical(self, "Close Error", f"Failed to close '{name}':\n{e}") + QtWidgets.QMessageBox.critical( + self, "Close Error", f"Failed to close '{name}':\n{e}" + ) return # remove from gui @@ -334,7 +359,6 @@ def closeInstrument(self, name: str):#, item: QtWidgets.QListWidgetItem): logger.info(f"Closed instrument '{name}'") - def removeInstrumentFromGui(self, name: str): """Remove an instrument from the station list.""" self.stationList.removeObject(name) diff --git a/src/instrumentserver/client/core.py b/src/instrumentserver/client/core.py index 6b0cd8c..870f018 100644 --- a/src/instrumentserver/client/core.py +++ b/src/instrumentserver/client/core.py @@ -11,7 +11,6 @@ logger = logging.getLogger(__name__) - class BaseClient: """Simple client for the StationServer. When a timeout happens, a RunTimeError is being raised. This error is there just to warn the user that a timeout @@ -25,7 +24,14 @@ class BaseClient: :param raise_exceptions: If true the client will raise an exception when the server sends one to it, defaults to True. """ - def __init__(self, host='localhost', port=DEFAULT_PORT, connect=True, timeout=20, raise_exceptions=True): + def __init__( + self, + host="localhost", + port=DEFAULT_PORT, + connect=True, + timeout=20, + raise_exceptions=True, + ): self.connected = False self._closed = False self.context = None @@ -68,7 +74,9 @@ def connect(self): self.context = zmq.Context() self.socket = self.context.socket(zmq.DEALER) self.socket.setsockopt(zmq.RCVTIMEO, self.recv_timeout_ms) - self.socket.setsockopt(zmq.IDENTITY, uuid.uuid4().hex.encode()) #todo: more meaningful id? + self.socket.setsockopt( + zmq.IDENTITY, uuid.uuid4().hex.encode() + ) # todo: more meaningful id? self.socket.connect(self.addr) self.connected = True @@ -89,7 +97,7 @@ def ask(self, message): else: logger.error("Server did not reply before timeout.") return None - + if isinstance(ret, ServerResponse): err = ret.error if err is not None: @@ -97,7 +105,7 @@ def ask(self, message): return ret.message return ret - + def _reset_connection(self): try: if self.socket is not None: @@ -106,7 +114,7 @@ def _reset_connection(self): self.connected = False if not self._closed: self.connect() - + def _handle_server_error(self, err): if isinstance(err, str): logger.error(err) @@ -123,7 +131,7 @@ def _handle_server_error(self, err): if self.raise_exceptions: raise TypeError(msg) logger.error(msg) - + def disconnect(self): self._closed = True if self.socket is not None: @@ -141,8 +149,7 @@ def disconnect(self): self.connected = False -def sendRequest(message, host='localhost', port=DEFAULT_PORT): +def sendRequest(message, host="localhost", port=DEFAULT_PORT): with BaseClient(host, port) as cli: ret = cli.ask(message) return ret - diff --git a/src/instrumentserver/client/proxy.py b/src/instrumentserver/client/proxy.py index cd92626..2f4aa03 100644 --- a/src/instrumentserver/client/proxy.py +++ b/src/instrumentserver/client/proxy.py @@ -4,6 +4,7 @@ @author: Chao """ + import inspect import json import yaml @@ -47,17 +48,20 @@ class ProxyMixin: - """ A simple mixin class for proxy objects.""" - - def __init__(self, *args, - cli: Optional["Client"] = None, - host: Optional[str] = 'localhost', - port: Optional[int] = DEFAULT_PORT, - remotePath: Optional[str] = None, - bluePrint: Optional[Union[ParameterBluePrint, - InstrumentModuleBluePrint, - MethodBluePrint]] = None, - **kwargs): + """A simple mixin class for proxy objects.""" + + def __init__( + self, + *args, + cli: Optional["Client"] = None, + host: Optional[str] = "localhost", + port: Optional[int] = DEFAULT_PORT, + remotePath: Optional[str] = None, + bluePrint: Optional[ + Union[ParameterBluePrint, InstrumentModuleBluePrint, MethodBluePrint] + ] = None, + **kwargs, + ): self.cli = cli self.host = host @@ -73,8 +77,7 @@ def __init__(self, *args, self.bp = bluePrint self.remotePath = self.bp.path else: - raise ValueError("Either `remotePath` or `bluePrint` must be " - "specified.") + raise ValueError("Either `remotePath` or `bluePrint` must be specified.") kwargs.update(self.initKwargsFromBluePrint(self.bp)) super().__init__(*args, **kwargs) @@ -90,18 +93,15 @@ def askServer(self, message: ServerInstruction): return sendRequest(message, self.host, self.port) def _getBluePrintFromServer(self, path): - req = ServerInstruction( - operation=Operation.get_blueprint, - requested_path=path - ) + req = ServerInstruction(operation=Operation.get_blueprint, requested_path=path) return self.askServer(req) def get_snapshot(self, *args, **kwargs): req = ServerInstruction( operation=Operation.call, call_spec=CallSpec( - target=self.remotePath + '.snapshot', args=args, kwargs=kwargs - ) + target=self.remotePath + ".snapshot", args=args, kwargs=kwargs + ), ) return self.askServer(req) @@ -119,48 +119,58 @@ class ProxyParameter(ProxyMixin, Parameter): priority. """ - def __init__(self, name: str, *args, - cli: Optional["Client"] = None, - host: Optional[str] = 'localhost', - port: Optional[int] = DEFAULT_PORT, - remotePath: Optional[str] = None, - bluePrint: Optional[ParameterBluePrint] = None, - setpoints_instrument: Optional[Instrument] = None, - **kwargs): - - super().__init__(name, *args, cli=cli, host=host, port=port, - remotePath=remotePath, bluePrint=bluePrint, - **kwargs) + def __init__( + self, + name: str, + *args, + cli: Optional["Client"] = None, + host: Optional[str] = "localhost", + port: Optional[int] = DEFAULT_PORT, + remotePath: Optional[str] = None, + bluePrint: Optional[ParameterBluePrint] = None, + setpoints_instrument: Optional[Instrument] = None, + **kwargs, + ): + + super().__init__( + name, + *args, + cli=cli, + host=host, + port=port, + remotePath=remotePath, + bluePrint=bluePrint, + **kwargs, + ) # add setpoints to parameter if we deal with ParameterWithSetpoints if self.bp.setpoints is not None and setpoints_instrument is not None: - setpoints = [getattr(setpoints_instrument, setpoint) for - setpoint in self.bp.setpoints] - setattr(self, 'setpoints', setpoints) + setpoints = [ + getattr(setpoints_instrument, setpoint) + for setpoint in self.bp.setpoints + ] + setattr(self, "setpoints", setpoints) def initKwargsFromBluePrint(self, bp): kwargs = {} if bp.settable: - kwargs['set_cmd'] = self._remoteSet + kwargs["set_cmd"] = self._remoteSet else: - kwargs['set_cmd'] = False + kwargs["set_cmd"] = False if bp.gettable: - kwargs['get_cmd'] = self._remoteGet + kwargs["get_cmd"] = self._remoteGet else: - kwargs['get_cmd'] = False - kwargs['unit'] = bp.unit + kwargs["get_cmd"] = False + kwargs["unit"] = bp.unit # FIXME: uncomment after implementing serializable validators # kwargs['vals'] = bp.vals - kwargs['docstring'] = bp.docstring + kwargs["docstring"] = bp.docstring return kwargs def _remoteSet(self, value: Any): msg = ServerInstruction( operation=Operation.call, - call_spec=CallSpec( - target=self.remotePath, - args=(value,) - ) + call_spec=CallSpec(target=self.remotePath, args=(value,)), ) return self.askServer(msg) @@ -169,7 +179,7 @@ def _remoteGet(self): operation=Operation.call, call_spec=CallSpec( target=self.remotePath, - ) + ), ) return self.askServer(msg) @@ -184,16 +194,28 @@ class ProxyInstrumentModule(ProxyMixin, InstrumentBase): :param port: The port number of the server. """ - def __init__(self, name: str, *args, - cli: Optional["Client"] = None, - host: Optional[str] = 'localhost', - port: Optional[int] = DEFAULT_PORT, - remotePath: Optional[str] = None, - bluePrint: Optional[InstrumentModuleBluePrint] = None, - **kwargs): - - super().__init__(name, *args, cli=cli, host=host, port=port, - remotePath=remotePath, bluePrint=bluePrint, **kwargs) + def __init__( + self, + name: str, + *args, + cli: Optional["Client"] = None, + host: Optional[str] = "localhost", + port: Optional[int] = DEFAULT_PORT, + remotePath: Optional[str] = None, + bluePrint: Optional[InstrumentModuleBluePrint] = None, + **kwargs, + ): + + super().__init__( + name, + *args, + cli=cli, + host=host, + port=port, + remotePath=remotePath, + bluePrint=bluePrint, + **kwargs, + ) # FIXME: This is not consistent with how mixin handles a `None` client. However, this seems like a more # elegant solution than any time we need the client to check to be None, start a new context client instead. @@ -201,14 +223,15 @@ def __init__(self, name: str, *args, self.cli = Client(host=host, port=port) for mn in self.bp.methods.keys(): - if mn == 'remove_parameter': + if mn == "remove_parameter": + def remove_parameter(obj, name: str): - obj.cli.call(f'{obj.remotePath}.remove_parameter', name) + obj.cli.call(f"{obj.remotePath}.remove_parameter", name) obj.update() self.remove_parameter = MethodType(remove_parameter, self) - self.parameters.pop('IDN', None) # we will redefine this later + self.parameters.pop("IDN", None) # we will redefine this later # When a new parameter or method is added to client, qcodes checks if that item exists or not. This is done # by calling __getattr__ method. The problem is that when that method gets called and cannot find that item it @@ -227,7 +250,6 @@ def _updating(self): finally: self.is_updating = old - def initKwargsFromBluePrint(self, bp): return {} @@ -237,8 +259,8 @@ def update(self): self._getProxyParameters() self._getProxyMethods() self._getProxySubmodules() - - def set_parameters(self, **param_dict:dict): + + def set_parameters(self, **param_dict: dict): """ Set instrument parameters in batch with a dict, keyed by parameter names. @@ -247,7 +269,9 @@ def set_parameters(self, **param_dict:dict): try: self.parameters[k](v) except KeyError: - raise KeyError(f"{self.bp.instrument_module_class} instrument does not have parameter '{k}'") + raise KeyError( + f"{self.bp.instrument_module_class} instrument does not have parameter '{k}'" + ) def add_parameter(self, name: str, *arg, **kw): """Add a parameter to the proxy instrument. @@ -259,7 +283,7 @@ def add_parameter(self, name: str, *arg, **kw): """ if name in self.parameters: - raise ValueError(f'Parameter: {name} already present in the proxy.') + raise ValueError(f"Parameter: {name} already present in the proxy.") bp: InstrumentModuleBluePrint if self.cli is None: @@ -294,8 +318,15 @@ def _getProxyParameters(self) -> None: if pn not in self.parameters: pbp = self.cli.getBluePrint(f"{self.remotePath}.{pn}") with self._updating(): - super().add_parameter(pbp.name, ProxyParameter, cli=self.cli, host=self.host, - port=self.port, bluePrint=pbp, setpoints_instrument=self) + super().add_parameter( + pbp.name, + ProxyParameter, + cli=self.cli, + host=self.host, + port=self.port, + bluePrint=pbp, + setpoints_instrument=self, + ) delKeys = [] for pn in self.parameters.keys(): @@ -321,7 +352,7 @@ def _makeProxyMethod(self, bp: MethodBluePrint): def wrap(*a, **k): msg = ServerInstruction( operation=Operation.call, - call_spec=CallSpec(target=bp.path, args=a, kwargs=k) + call_spec=CallSpec(target=bp.path, args=a, kwargs=k), ) return self.askServer(msg) @@ -331,8 +362,11 @@ def wrap(*a, **k): # FIXME: a better solution to this would probably be to convet kind into the enum object. But it seems that the # parameter kind enum is private. for pn, kind in params.items(): - if kind in [str(inspect.Parameter.POSITIONAL_OR_KEYWORD), str(inspect.Parameter.POSITIONAL_ONLY)]: - args.append(f'{pn}') + if kind in [ + str(inspect.Parameter.POSITIONAL_OR_KEYWORD), + str(inspect.Parameter.POSITIONAL_ONLY), + ]: + args.append(f"{pn}") elif kind == str(inspect.Parameter.VAR_POSITIONAL): args.append(f"*{pn}") elif kind == str(inspect.Parameter.KEYWORD_ONLY): @@ -342,13 +376,13 @@ def wrap(*a, **k): # we need to add a `self` argument because we want this to be a bound # method of the instrument instance. - sig = sig[0] + 'self, ' + sig[1:] + sig = sig[0] + "self, " + sig[1:] new_func_str = f"""from typing import *\ndef {bp.name}{sig}: - return wrap({', '.join(args)})""" + return wrap({", ".join(args)})""" # make sure the method knows the wrap function. # TODO: this is not complete! - globs = {'wrap': wrap, 'qcodes': qc, 'collections': collections} + globs = {"wrap": wrap, "qcodes": qc, "collections": collections} _ret = exec(new_func_str, globs) fun = globs[bp.name] fun.__doc__ = bp.docstring @@ -361,7 +395,8 @@ def _getProxySubmodules(self): for sn, s in self.bp.submodules.items(): if sn not in self.submodules: submodule = ProxyInstrumentModule( - s.name, cli=self.cli, host=self.host, port=self.port, bluePrint=s) + s.name, cli=self.cli, host=self.host, port=self.port, bluePrint=s + ) self.add_submodule(sn, submodule) else: self.submodules[sn].update() @@ -384,7 +419,8 @@ def _refreshProxySubmodules(self): for sn, s in self.bp.submodules.items(): if sn not in self.submodules: submodule = ProxyInstrumentModule( - s.name, cli=self.cli, host=self.host, port=self.port, bluePrint=s) + s.name, cli=self.cli, host=self.host, port=self.port, bluePrint=s + ) self.add_submodule(sn, submodule) else: self.submodules[sn].update() @@ -414,24 +450,41 @@ def __getattr__(self, item): class Client(BaseClient): """Client with common server requests as convenience functions.""" - def __init__(self, host='localhost', port=DEFAULT_PORT, connect=True, timeout=20, raise_exceptions=True): + + def __init__( + self, + host="localhost", + port=DEFAULT_PORT, + connect=True, + timeout=20, + raise_exceptions=True, + ): super().__init__(host, port, connect, timeout, raise_exceptions) self._bp_cache = {} self._bp_cache_lock = threading.Lock() def list_instruments(self) -> Dict[str, str]: - """ Get the existing instruments on the server. - """ + """Get the existing instruments on the server.""" message = ServerInstruction(operation=Operation.get_existing_instruments) try: return self.ask(message) except Exception as e: - logger.error(f"Failed to send or receive message to server at {self.host}:{self.port}", exc_info=True) - raise RuntimeError("Communication with server failed. See logs for details.") from e - - def find_or_create_instrument(self, name: str, instrument_class: Optional[str] = None, - *args: Any, **kwargs: Any) -> ProxyInstrumentModule: - """ Looks for an instrument in the server. If it cannot find it, create a new instrument on the server. Returns + logger.error( + f"Failed to send or receive message to server at {self.host}:{self.port}", + exc_info=True, + ) + raise RuntimeError( + "Communication with server failed. See logs for details." + ) from e + + def find_or_create_instrument( + self, + name: str, + instrument_class: Optional[str] = None, + *args: Any, + **kwargs: Any, + ) -> ProxyInstrumentModule: + """Looks for an instrument in the server. If it cannot find it, create a new instrument on the server. Returns a proxy for either the found or the new instrument. :param name: Name of the new instrument. @@ -446,27 +499,26 @@ def find_or_create_instrument(self, name: str, instrument_class: Optional[str] = return ProxyInstrumentModule(name=name, cli=self, remotePath=name) if instrument_class is None: - raise ValueError('Need a class to create a new instrument.') + raise ValueError("Need a class to create a new instrument.") if not isinstance(instrument_class, str): - raise TypeError('Class name must be a string with the import path of the class. ' - 'If trying to start the parameter manager for example use "instrumentserver.params.ParameterManager" instead of ' - 'passing the class itself.') + raise TypeError( + "Class name must be a string with the import path of the class. " + 'If trying to start the parameter manager for example use "instrumentserver.params.ParameterManager" instead of ' + "passing the class itself." + ) req = ServerInstruction( operation=Operation.create_instrument, create_instrument_spec=InstrumentCreationSpec( - instrument_class=instrument_class, - name=name, - args=args, - kwargs=kwargs - ) + instrument_class=instrument_class, name=name, args=args, kwargs=kwargs + ), ) _ = self.ask(req) return ProxyInstrumentModule(name=name, cli=self, remotePath=name) def close_instrument(self, instrument_name: str): - self.call('close_and_remove_instrument', instrument_name) + self.call("close_and_remove_instrument", instrument_name) def call(self, target, *args, **kwargs): msg = ServerInstruction( @@ -475,7 +527,7 @@ def call(self, target, *args, **kwargs): target=target, args=args, kwargs=kwargs, - ) + ), ) return self.ask(msg) @@ -513,22 +565,27 @@ def invalidateBlueprint(self, path=None): self._bp_cache.clear() else: for k in list(self._bp_cache): - if k == path or k.startswith(path + '.'): + if k == path or k.startswith(path + "."): del self._bp_cache[k] def get_snapshot(self, instrument: str | None = None, *args, **kwargs): msg = ServerInstruction( operation=Operation.call, call_spec=CallSpec( - target='snapshot' if instrument is None else f"{instrument}.snapshot", + target="snapshot" if instrument is None else f"{instrument}.snapshot", args=args, kwargs=kwargs, - ) + ), ) return self.ask(msg) - def getParamDict(self, instrument: str | None = None, - attrs: List[str] = ['value'], *args, **kwargs): + def getParamDict( + self, + instrument: str | None = None, + attrs: List[str] = ["value"], + *args, + **kwargs, + ): msg = ServerInstruction( operation=Operation.get_param_dict, serialization_opts=ParameterSerializeSpec( @@ -536,11 +593,13 @@ def getParamDict(self, instrument: str | None = None, attrs=attrs, args=args, kwargs=kwargs, - ) + ), ) return self.ask(msg) - def paramsToFile(self, filePath: str, instruments: Optional[List[str]] = None, *args, **kwargs): + def paramsToFile( + self, filePath: str, instruments: Optional[List[str]] = None, *args, **kwargs + ): filePath = os.path.abspath(filePath) folder, file = os.path.split(filePath) @@ -550,7 +609,9 @@ def paramsToFile(self, filePath: str, instruments: Optional[List[str]] = None, * else: params = {} for instrument_name in instruments: - inst_params = self.getParamDict(instrument=instrument_name, *args, **kwargs) + inst_params = self.getParamDict( + instrument=instrument_name, *args, **kwargs + ) params.update(inst_params) # Convert to nested format before saving, @@ -558,7 +619,7 @@ def paramsToFile(self, filePath: str, instruments: Optional[List[str]] = None, * params = flat_to_nested_dict(params) if not os.path.exists(folder): os.makedirs(folder) - with open(filePath, 'w') as f: + with open(filePath, "w") as f: json.dump(params, f, indent=2, sort_keys=True) def setParameters(self, parameters: Dict[str, Any]): @@ -571,7 +632,7 @@ def setParameters(self, parameters: Dict[str, Any]): def paramsFromFile(self, filePath: str, instruments: Optional[List[str]] = None): params = None if os.path.exists(filePath): - with open(filePath, 'r') as f: + with open(filePath, "r") as f: params = json.load(f) # Convert to flat format before sending to server (setParameters expects flat) if not is_flat_dict(params): @@ -582,7 +643,10 @@ def paramsFromFile(self, filePath: str, instruments: Optional[List[str]] = None) filtered_params = {} for instrument_name in instruments: for key, value in params.items(): - if key.startswith(instrument_name + '.') or key == instrument_name: + if ( + key.startswith(instrument_name + ".") + or key == instrument_name + ): filtered_params[key] = value params = filtered_params @@ -591,14 +655,18 @@ def paramsFromFile(self, filePath: str, instruments: Optional[List[str]] = None) logger.warning(f"File {filePath} does not exist. No params loaded.") def _getGuiConfig(self, instrumentName: str) -> Dict[str, Any]: - """ Gets the GUI config for an instrument from the object. Should only be used in a detached server GUI.""" - msg = ServerInstruction(operation=Operation.get_gui_config, requested_path=instrumentName) + """Gets the GUI config for an instrument from the object. Should only be used in a detached server GUI.""" + msg = ServerInstruction( + operation=Operation.get_gui_config, requested_path=instrumentName + ) return self.ask(msg) + class SubClient(QtCore.QObject): """ Specific subscription client used for real-time parameter updates. """ + #: Signal(ParameterBroadcastBluePrint) -- #: emitted when the server broadcast either a new parameter or an update to an existing one. update = QtCore.Signal(ParameterBroadcastBluePrint) @@ -606,7 +674,12 @@ class SubClient(QtCore.QObject): #: Signal emitted when the listener finishes (for proper cleanup) finished = QtCore.Signal() - def __init__(self, instruments: Optional[List[str]] = None, sub_host: str = 'localhost', sub_port: int = DEFAULT_PORT + 1): + def __init__( + self, + instruments: Optional[List[str]] = None, + sub_host: str = "localhost", + sub_port: int = DEFAULT_PORT + 1, + ): """ Creates a new subscription client. @@ -643,7 +716,7 @@ def connect(self): # subscribe to the specified instruments if self.instruments is None: - self._sock.setsockopt_string(zmq.SUBSCRIBE, '') + self._sock.setsockopt_string(zmq.SUBSCRIBE, "") else: for ins in self.instruments: self._sock.setsockopt_string(zmq.SUBSCRIBE, ins) @@ -694,12 +767,15 @@ def __init__(self, parent, *arg, **kw): class QtClient(_QtAdapter, Client): - def __init__(self, parent=None, - host='localhost', - port=DEFAULT_PORT, - connect=True, - timeout=5, - raise_exceptions=True): + def __init__( + self, + parent=None, + host="localhost", + port=DEFAULT_PORT, + connect=True, + timeout=5, + raise_exceptions=True, + ): # Calling the parents like this ensures that the arguments arrive to the parents properly. _QtAdapter.__init__(self, parent=parent) Client.__init__(self, host, port, connect, timeout, raise_exceptions) @@ -715,9 +791,16 @@ def disconnect(self, *args, **kwargs): class ClientStation: - def __init__(self, host='localhost', port=DEFAULT_PORT, connect=True, timeout=20, raise_exceptions=True, - config_path: str = None, - param_path: str = None): + def __init__( + self, + host="localhost", + port=DEFAULT_PORT, + connect=True, + timeout=20, + raise_exceptions=True, + config_path: str = None, + param_path: str = None, + ): """ A lightweight container for managing a collection of proxy instruments on the client side. @@ -771,6 +854,7 @@ def __init__(self, host='localhost', port=DEFAULT_PORT, connect=True, timeout=20 if config_path is not None: # Use config.py to parse server config format from instrumentserver.config import loadConfig + _, serverConfig, fullConfig, tempFile, _, _ = loadConfig(config_path) tempFile.close() # Clean up temp file @@ -781,8 +865,13 @@ def __init__(self, host='localhost', port=DEFAULT_PORT, connect=True, timeout=20 self._config_path = config_path def _make_client(self, connect=True): - cli = Client(host=self._host, port=self._port, connect=connect, - timeout=self._timeout, raise_exceptions=self._raise_exceptions) + cli = Client( + host=self._host, + port=self._port, + connect=connect, + timeout=self._timeout, + raise_exceptions=self._raise_exceptions, + ) return cli def _create_instruments(self, instrument_dict: dict): @@ -792,20 +881,19 @@ def _create_instruments(self, instrument_dict: dict): """ for name, conf in instrument_dict.items(): # Extract type (None if not present - will just get existing instrument) - instrument_class = conf.get('type') + instrument_class = conf.get("type") # Pass all other fields as kwargs (except server/GUI-specific fields) - kwargs = {k: v for k, v in conf.items() - if k not in ['type', 'initialize', 'gui']} + kwargs = { + k: v for k, v in conf.items() if k not in ["type", "initialize", "gui"] + } instrument = self.client.find_or_create_instrument( - name=name, - instrument_class=instrument_class, - **kwargs + name=name, instrument_class=instrument_class, **kwargs ) self.instruments[name] = instrument - def close_instrument(self, instrument_name:str): + def close_instrument(self, instrument_name: str): self.client.close_instrument(instrument_name) def disconnect(self): @@ -827,7 +915,10 @@ def wrapper(self, *args, **kwargs): try: retval = func(self, *args, **kwargs) except Exception as e: - logger.error(f"Error calling {func}: {e}. Trying to remake instrument client ", exc_info=True) + logger.error( + f"Error calling {func}: {e}. Trying to remake instrument client ", + exc_info=True, + ) self.client = self._make_client(connect=True) self._create_instruments(self.full_config) logger.info(f"Successfully remade instrument client.") @@ -836,9 +927,14 @@ def wrapper(self, *args, **kwargs): return wrapper - def find_or_create_instrument(self, name: str, instrument_class: Optional[str] = None, - *args: Any, **kwargs: Any) -> ProxyInstrumentModule: - """ Looks for an instrument in the server. If it cannot find it, create a new instrument on the server. Returns + def find_or_create_instrument( + self, + name: str, + instrument_class: Optional[str] = None, + *args: Any, + **kwargs: Any, + ) -> ProxyInstrumentModule: + """Looks for an instrument in the server. If it cannot find it, create a new instrument on the server. Returns a proxy for either the found or the new instrument. :param name: Name of the new instrument. @@ -849,7 +945,9 @@ def find_or_create_instrument(self, name: str, instrument_class: Optional[str] = :returns: A new virtual instrument. """ - ins = self.client.find_or_create_instrument(name, instrument_class, *args, **kwargs) + ins = self.client.find_or_create_instrument( + name, instrument_class, *args, **kwargs + ) self.instruments[name] = ins return ins @@ -894,13 +992,17 @@ def set_parameters(self, inst_params: Dict): if k in self.instruments: params_set[k] = inst_params[k] else: - logger.warning(f"Instrument {k} parameter neglected, as it doesn't belong to this station") + logger.warning( + f"Instrument {k} parameter neglected, as it doesn't belong to this station" + ) # the client `setParameters` function requires a flat param dict self.client.setParameters(flatten_dict(params_set)) @_remake_client_station_when_fail - def save_parameters(self, file_path: str = None, select_instruments:List[str] = None): + def save_parameters( + self, file_path: str = None, select_instruments: List[str] = None + ): """ Save instrument parameters to a JSON file in nested format. @@ -908,12 +1010,16 @@ def save_parameters(self, file_path: str = None, select_instruments:List[str] = :param select_instruments: list of instrument names to save. If None, all instruments in this station are saved. """ file_path = file_path if file_path is not None else self.param_path - instruments = select_instruments if select_instruments is not None else list(self.instruments.keys()) + instruments = ( + select_instruments + if select_instruments is not None + else list(self.instruments.keys()) + ) # Delegate to client's paramsToFile self.client.paramsToFile(file_path, instruments=instruments) @_remake_client_station_when_fail - def load_parameters(self, file_path: str, select_instruments:List[str] = None): + def load_parameters(self, file_path: str, select_instruments: List[str] = None): """ Load instrument parameters from a JSON file. @@ -922,9 +1028,12 @@ def load_parameters(self, file_path: str, select_instruments:List[str] = None): """ file_path = file_path if file_path is not None else self.param_path # Delegate to client's paramsFromFile - instruments = select_instruments if select_instruments is not None else list(self.instruments.keys()) + instruments = ( + select_instruments + if select_instruments is not None + else list(self.instruments.keys()) + ) self.client.paramsFromFile(file_path, instruments=instruments) def __getitem__(self, item): return self.instruments[item] - diff --git a/src/instrumentserver/config.py b/src/instrumentserver/config.py index f389e67..e5417b7 100644 --- a/src/instrumentserver/config.py +++ b/src/instrumentserver/config.py @@ -3,6 +3,7 @@ to the config as defaults. If you are adding any extra fields to the config make sure to add the default values on those variables since we parse the config using those. """ + import io import tempfile from typing import IO, Any @@ -11,10 +12,10 @@ from pathlib import Path # Centralised point of extra fields for the server with its default as value -SERVERFIELDS = {'initialize': True} +SERVERFIELDS = {"initialize": True} # Extra fields for the GUI. -GUIFIELD = {'type': 'instrumentserver.gui.instruments.GenericInstrument', 'kwargs': {}} +GUIFIELD = {"type": "instrumentserver.gui.instruments.GenericInstrument", "kwargs": {}} def loadConfig(configPath: str | Path) -> tuple[str, dict, dict, IO[bytes], dict, dict]: @@ -36,7 +37,7 @@ def loadConfig(configPath: str | Path) -> tuple[str, dict, dict, IO[bytes], dict guiConfig = {} # Individual gui config of each instrument fullConfig = {} # serverConfig + guiConfig + any unfilled fields. Used for creating instruments from the gui pollingRates = {} # Polling rates for each parameter - ipAddresses = {} # Dictionary of IP Addresses to send broadcasts to: + ipAddresses = {} # Dictionary of IP Addresses to send broadcasts to: # externalBroadcast: where to externally send parameter change broadcasts to, formatted like "tcp://address:port" # listeningAddress: additional address to listen to messages received by the server, formatted like "address" @@ -44,17 +45,19 @@ def loadConfig(configPath: str | Path) -> tuple[str, dict, dict, IO[bytes], dict rawConfig = yaml.load(configPath) if "instruments" not in rawConfig: - raise AttributeError("All configurations must be inside the 'instruments' field. " - "Try adding 'instruments:' at the top of the config file and " - "indenting everything underneath.") + raise AttributeError( + "All configurations must be inside the 'instruments' field. " + "Try adding 'instruments:' at the top of the config file and " + "indenting everything underneath." + ) # Parse gui_defaults section (class-based GUI configuration) gui_defaults = {} - if 'gui_defaults' in rawConfig: - gui_defaults = rawConfig.pop('gui_defaults') + if "gui_defaults" in rawConfig: + gui_defaults = rawConfig.pop("gui_defaults") # Removing any extra fields - for instrumentName, configDict in rawConfig['instruments'].items(): + for instrumentName, configDict in rawConfig["instruments"].items(): serverConfig[instrumentName] = {} for field, default in SERVERFIELDS.items(): if field in configDict: @@ -67,49 +70,64 @@ def loadConfig(configPath: str | Path) -> tuple[str, dict, dict, IO[bytes], dict # we don't go through the entire gui because generic is a special setting # and we only have 2 different options for now - if 'gui' in configDict: - guiDict = configDict.pop('gui') + if "gui" in configDict: + guiDict = configDict.pop("gui") if guiDict is None: raise AttributeError(f'"gui" field cannot be None') - if 'type' in guiDict: - if guiDict['type'] == 'generic' or guiDict['type'] == 'Generic': - guiDict['type'] = GUIFIELD['type'] + if "type" in guiDict: + if guiDict["type"] == "generic" or guiDict["type"] == "Generic": + guiDict["type"] = GUIFIELD["type"] # If the user does not specify a gui, the default one will be used else: - guiDict['type'] = GUIFIELD['type'] + guiDict["type"] = GUIFIELD["type"] guiConfig[instrumentName] = guiDict else: guiConfig[instrumentName] = GUIFIELD - if 'pollingRate' in configDict: - ratesDict = configDict.pop('pollingRate') + if "pollingRate" in configDict: + ratesDict = configDict.pop("pollingRate") # This catches the case when the pollingRate is in the config but it is empty. if isinstance(ratesDict, dict): - pollingRates.update({instrumentName + "." + param: rate for param, rate in ratesDict.items()}) - - fullConfig[instrumentName] = {'gui': guiConfig[instrumentName], **configDict, **serverConfig[instrumentName]} + pollingRates.update( + { + instrumentName + "." + param: rate + for param, rate in ratesDict.items() + } + ) + + fullConfig[instrumentName] = { + "gui": guiConfig[instrumentName], + **configDict, + **serverConfig[instrumentName], + } # Merge gui_defaults into guiConfig for each instrument if gui_defaults: for instrumentName in guiConfig.keys(): # Get instrument class name from the type field - instrument_type = fullConfig[instrumentName].get('type', '') - class_name = instrument_type.split('.')[-1] if instrument_type else '' + instrument_type = fullConfig[instrumentName].get("type", "") + class_name = instrument_type.split(".")[-1] if instrument_type else "" # Initialize kwargs if not present - if 'kwargs' not in guiConfig[instrumentName]: - guiConfig[instrumentName]['kwargs'] = {} + if "kwargs" not in guiConfig[instrumentName]: + guiConfig[instrumentName]["kwargs"] = {} # Merge patterns in order: __default__ → class → instance # For each GUI config key (parameters-hide, methods-hide, etc.) - for config_key in ['parameters-hide', 'methods-hide', 'parameters-star', 'parameters-trash', - 'methods-star', 'methods-trash']: + for config_key in [ + "parameters-hide", + "methods-hide", + "parameters-star", + "parameters-trash", + "methods-star", + "methods-trash", + ]: merged_patterns = [] # 1. Add patterns from __default__ - if '__default__' in gui_defaults: - default_config = gui_defaults['__default__'] + if "__default__" in gui_defaults: + default_config = gui_defaults["__default__"] if config_key in default_config: merged_patterns.extend(default_config[config_key]) @@ -120,23 +138,25 @@ def loadConfig(configPath: str | Path) -> tuple[str, dict, dict, IO[bytes], dict merged_patterns.extend(class_config[config_key]) # 3. Add patterns from instance-specific config - if config_key in guiConfig[instrumentName]['kwargs']: - merged_patterns.extend(guiConfig[instrumentName]['kwargs'][config_key]) + if config_key in guiConfig[instrumentName]["kwargs"]: + merged_patterns.extend( + guiConfig[instrumentName]["kwargs"][config_key] + ) # Store merged patterns if any exist if merged_patterns: - guiConfig[instrumentName]['kwargs'][config_key] = merged_patterns + guiConfig[instrumentName]["kwargs"][config_key] = merged_patterns # Update fullConfig with merged GUI config - fullConfig[instrumentName]['gui'] = guiConfig[instrumentName] + fullConfig[instrumentName]["gui"] = guiConfig[instrumentName] # Gets all of the broadcasting and listening addresses from the config file - if 'networking' in rawConfig: - addressDict = rawConfig['networking'] + if "networking" in rawConfig: + addressDict = rawConfig["networking"] if addressDict is not None: for address in addressDict.items(): ipAddresses.update({address[0]: address[1]}) - rawConfig.pop('networking') + rawConfig.pop("networking") # Creating the file like object with io.BytesIO() as ioBytesFile: diff --git a/src/instrumentserver/gui/__init__.py b/src/instrumentserver/gui/__init__.py index c9406ef..05a6a8a 100644 --- a/src/instrumentserver/gui/__init__.py +++ b/src/instrumentserver/gui/__init__.py @@ -6,12 +6,12 @@ def getStyleSheet(): if f.open(QtCore.QIODevice.ReadOnly | QtCore.QIODevice.Text): style = f.readAll() f.close() - return str(style, 'utf-8') + return str(style, "utf-8") def widgetDialog(w: QtWidgets.QWidget): dg = QtWidgets.QDialog() - dg.setWindowTitle('instrumentserver') + dg.setWindowTitle("instrumentserver") dg.setWindowFlag(QtCore.Qt.WindowMinimizeButtonHint) dg.setWindowFlag(QtCore.Qt.WindowMaximizeButtonHint) dg.widget = w # type: ignore[attr-defined] # I am pretty sure the stubs are wrong for this one. @@ -28,7 +28,7 @@ def widgetDialog(w: QtWidgets.QWidget): return dg -def widgetMainWindow(w: QtWidgets.QWidget, name: str = 'instrumentserver'): +def widgetMainWindow(w: QtWidgets.QWidget, name: str = "instrumentserver"): mw = QtWidgets.QMainWindow() mw.setWindowTitle(name) mw.setCentralWidget(w) @@ -42,7 +42,7 @@ def widgetMainWindow(w: QtWidgets.QWidget, name: str = 'instrumentserver'): def keepSmallHorizontally(w: QtWidgets.QWidget): w.setSizePolicy( - QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, - QtWidgets.QSizePolicy.Minimum) + QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum + ) ) - diff --git a/src/instrumentserver/gui/base_instrument.py b/src/instrumentserver/gui/base_instrument.py index 77bfa7c..996cb12 100644 --- a/src/instrumentserver/gui/base_instrument.py +++ b/src/instrumentserver/gui/base_instrument.py @@ -121,7 +121,14 @@ class ItemBase(QtGui.QStandardItem): If this is None, it means that the item is a submodule and should only be there to store the children. """ - def __init__(self, name, star=False, trash=False, showDelegate=True, element=None,): + def __init__( + self, + name, + star=False, + trash=False, + showDelegate=True, + element=None, + ): super().__init__() self.name = name @@ -175,6 +182,7 @@ class InstrumentModelBase(QtGui.QStandardItemModel): :param itemsHide: List of items that will not be loaded. If the user adds the same parameters to the model manually, they will be shown. """ + #: Signal(ItemBase) #: Gets emitted after a new item has been added. The user is in charge of emitting it in their implementation #: of addChildTo @@ -184,13 +192,16 @@ class InstrumentModelBase(QtGui.QStandardItemModel): #: Emitted when the model refreshes. modelRefreshed = QtCore.Signal() - def __init__(self, instrument, - attr: str, - itemClass: type[ItemBase] = ItemBase, - itemsStar:Optional[List[str]] = [], - itemsTrash: Optional[List[str]] = [], - itemsHide: Optional[List[str]] = [], - parent: Optional[QtCore.QObject] = None,): + def __init__( + self, + instrument, + attr: str, + itemClass: type[ItemBase] = ItemBase, + itemsStar: Optional[List[str]] = [], + itemsTrash: Optional[List[str]] = [], + itemsHide: Optional[List[str]] = [], + parent: Optional[QtCore.QObject] = None, + ): super().__init__(parent=parent) @@ -246,9 +257,11 @@ def loadItems(self, module=None, prefix=None): # addItem only requires fullName, everything else is going to be passed as args and kwargs to the item # constructor if prefix is not None: - objectName = '.'.join([prefix, objectName]) + objectName = ".".join([prefix, objectName]) if not self._matches_any_pattern(objectName, self.itemsHide): - item = self.addItem(fullName=objectName, star=False, trash=False, element=obj) + item = self.addItem( + fullName=objectName, star=False, trash=False, element=obj + ) if self._matches_any_pattern(objectName, self.itemsTrash): self.onItemTrashToggle(item) if self._matches_any_pattern(objectName, self.itemsStar): @@ -256,7 +269,7 @@ def loadItems(self, module=None, prefix=None): for submodName, submod in module.submodules.items(): if prefix is not None: - submodName = '.'.join([prefix, submodName]) + submodName = ".".join([prefix, submodName]) self.loadItems(submod, submodName) def refreshAll(self): @@ -286,8 +299,8 @@ def addItem(self, fullName, **kwargs): :param fullName: The name of the parameter """ - path = fullName.split('.')[:-1] - paramName = fullName.split('.')[-1] + path = fullName.split(".")[:-1] + paramName = fullName.split(".")[-1] parent = self smName = None @@ -297,10 +310,18 @@ def addItem(self, fullName, **kwargs): else: smName = smName + f".{sm}" - items = self.findItems(smName, QtCore.Qt.MatchExactly | QtCore.Qt.MatchRecursive, 0) + items = self.findItems( + smName, QtCore.Qt.MatchExactly | QtCore.Qt.MatchRecursive, 0 + ) if len(items) == 0: - subModItem = self.itemClass(name=smName, star=False, trash=False, showDelegate=False, element=None) + subModItem = self.itemClass( + name=smName, + star=False, + trash=False, + showDelegate=False, + element=None, + ) # submodules get directly added here and not in the load function, so need to have it here too. if self.loadingItems: if not self._matches_any_pattern(smName, self.itemsHide): @@ -321,7 +342,9 @@ def addItem(self, fullName, **kwargs): return newItem def removeItem(self, fullName): - items = self.findItems(fullName, QtCore.Qt.MatchExactly | QtCore.Qt.MatchRecursive, 0) + items = self.findItems( + fullName, QtCore.Qt.MatchExactly | QtCore.Qt.MatchRecursive, 0 + ) if len(items) > 0: item = items[0] @@ -342,7 +365,7 @@ def onItemStarToggle(self, item): else: item.star = True item.trash = False - item.setIcon(QtGui.QIcon(':/icons/star.svg')) + item.setIcon(QtGui.QIcon(":/icons/star.svg")) @QtCore.Slot(ItemBase) def onItemTrashToggle(self, item): @@ -353,11 +376,10 @@ def onItemTrashToggle(self, item): else: item.trash = True item.star = False - item.setIcon(QtGui.QIcon(':/icons/trash.svg')) + item.setIcon(QtGui.QIcon(":/icons/trash.svg")) class InstrumentSortFilterProxyModel(QtCore.QSortFilterProxyModel): - #: Signal() #: Emitted before a filter occurs filterIncoming = QtCore.Signal() @@ -366,7 +388,9 @@ class InstrumentSortFilterProxyModel(QtCore.QSortFilterProxyModel): #: Emitted after a filter has occurred. filterFinished = QtCore.Signal() - def __init__(self, sourceModel: InstrumentModelBase, parent: Optional[QtCore.QObject] = None): + def __init__( + self, sourceModel: InstrumentModelBase, parent: Optional[QtCore.QObject] = None + ): super().__init__(parent=parent) self.setSourceModel(sourceModel) @@ -428,7 +452,9 @@ def _isParentTrash(self, parent): return self._isParentTrash(parent.parent()) - def filterAcceptsRow(self, source_row: int, source_parent: QtCore.QModelIndex) -> bool: + def filterAcceptsRow( + self, source_row: int, source_parent: QtCore.QModelIndex + ) -> bool: """ Calls for the super() unless trash is active and the item or one of its parent is trash. """ @@ -442,12 +468,14 @@ def filterAcceptsRow(self, source_row: int, source_parent: QtCore.QModelIndex) - # The order in which things get constructed seems to impact this. # When the application is first starting, the proxy model does not have the trash attribute. - if hasattr(self, 'trash'): + if hasattr(self, "trash"): if self.trash: # Assertion is there to satisfy mypy. item can be None, that is why we check before making the assertion if item is not None: assert isinstance(item, ItemBase) - if self._isParentTrash(parent) or getattr(item, "trash", False): # item could be None when it's trashed and hidden + if self._isParentTrash(parent) or getattr( + item, "trash", False + ): # item could be None when it's trashed and hidden return False return super().filterAcceptsRow(source_row, source_parent) @@ -459,13 +487,13 @@ def lessThan(self, left: QtCore.QModelIndex, right: QtCore.QModelIndex) -> bool: # The order in which things get constructed seems to impact this. # When the application is first starting, the proxy model does not have the star attribute. - if hasattr(self, 'star'): + if hasattr(self, "star"): if self.star: model = self.sourceModel() assert isinstance(model, InstrumentModelBase) leftItem = model.itemFromIndex(left) rightItem = model.itemFromIndex(right) - if hasattr(leftItem, 'star') and hasattr(rightItem, 'star'): + if hasattr(leftItem, "star") and hasattr(rightItem, "star"): if self.sortOrder() == QtCore.Qt.DescendingOrder: if rightItem.star and not leftItem.star: return True @@ -482,7 +510,6 @@ def lessThan(self, left: QtCore.QModelIndex, right: QtCore.QModelIndex) -> bool: class InstrumentTreeViewBase(QtWidgets.QTreeView): - #: Signal(ItemBase) #: emitted when this item got its trashed action triggered. itemTrashToggle = QtCore.Signal(ItemBase) @@ -491,7 +518,12 @@ class InstrumentTreeViewBase(QtWidgets.QTreeView): #: emitted when this item got its star action triggered. itemStarToggle = QtCore.Signal(ItemBase) - def __init__(self, model, delegateColumns: Optional[List[int]]=None, parent: Optional[QtWidgets.QWidget] = None): + def __init__( + self, + model, + delegateColumns: Optional[List[int]] = None, + parent: Optional[QtWidgets.QWidget] = None, + ): super().__init__(parent=parent) # Indicates if a column is using delegates. @@ -509,7 +541,7 @@ def __init__(self, model, delegateColumns: Optional[List[int]]=None, parent: Opt # the real model m = self.model() assert isinstance(m, InstrumentSortFilterProxyModel) - assert hasattr(m, 'sourceModel') + assert hasattr(m, "sourceModel") self.modelActual = m.sourceModel() # We need to turn sorting off so that the view sorting does not interfere with the proxy model sorting. @@ -521,14 +553,14 @@ def __init__(self, model, delegateColumns: Optional[List[int]]=None, parent: Opt self.setAlternatingRowColors(True) - self.starIcon = QtGui.QIcon(':/icons/star.svg') - self.starCrossedIcon = QtGui.QIcon(':/icons/star-crossed.svg') - self.trashIcon = QtGui.QIcon(':/icons/trash.svg') - self.trashCrossedIcon = QtGui.QIcon(':/icons/trash-crossed') + self.starIcon = QtGui.QIcon(":/icons/star.svg") + self.starCrossedIcon = QtGui.QIcon(":/icons/star-crossed.svg") + self.trashIcon = QtGui.QIcon(":/icons/trash.svg") + self.trashCrossedIcon = QtGui.QIcon(":/icons/trash-crossed") - self.starItemAction = QtWidgets.QAction(self.starIcon, 'Star Item') + self.starItemAction = QtWidgets.QAction(self.starIcon, "Star Item") self.starItemAction.triggered.connect(self.onStarActionTrigger) - self.trashItemAction = QtWidgets.QAction(self.trashIcon, 'Trash Item') + self.trashItemAction = QtWidgets.QAction(self.trashIcon, "Trash Item") self.trashItemAction.triggered.connect(self.onTrashActionTrigger) self.contextMenu = QtWidgets.QMenu(self) @@ -539,7 +571,7 @@ def __init__(self, model, delegateColumns: Optional[List[int]]=None, parent: Opt self.customContextMenuRequested.connect(self.onContextMenuRequested) @QtCore.Slot() - def fillCollapsedDict(self, parentItem: Optional[ItemBase]=None): + def fillCollapsedDict(self, parentItem: Optional[ItemBase] = None): """ Fills the collapsed state dictionary to be recovered after a filter event occured. """ @@ -576,14 +608,24 @@ def restoreCollapsedDict(self): the persistent editors and triggers a resizing of delegates. """ for persistentIndex, state in self.collapsedState.items(): - modelIndex = self.modelActual.index(persistentIndex.row(), persistentIndex.column(), persistentIndex.parent()) + modelIndex = self.modelActual.index( + persistentIndex.row(), + persistentIndex.column(), + persistentIndex.parent(), + ) item = self.modelActual.itemFromIndex(modelIndex) proxyIndex = self.model().mapFromSource(modelIndex) self.setExpanded(proxyIndex, state) if item.showDelegate: - delegateIndexes = [self.modelActual.index(persistentIndex.row(), x, persistentIndex.parent()) for x in - self.delegateColumns] - proxyDelegateIndexes = [self.model().mapFromSource(index) for index in delegateIndexes] + delegateIndexes = [ + self.modelActual.index( + persistentIndex.row(), x, persistentIndex.parent() + ) + for x in self.delegateColumns + ] + proxyDelegateIndexes = [ + self.model().mapFromSource(index) for index in delegateIndexes + ] for delegateIndex in proxyDelegateIndexes: self.openPersistentEditor(delegateIndex) self.scheduleDelayedItemsLayout() @@ -598,21 +640,31 @@ def setAllDelegatesPersistent(self, parentIndex=None): for i in range(self.model().rowCount()): for column in self.delegateColumns: index = self.model().index(i, column) - index0 = self.model().index(i, 0) # Only items at column 0 hold children and model info - item0 = self.modelActual.itemFromIndex(self.model().mapToSource(index0)) + index0 = self.model().index( + i, 0 + ) # Only items at column 0 hold children and model info + item0 = self.modelActual.itemFromIndex( + self.model().mapToSource(index0) + ) if item0.showDelegate: self.openPersistentEditor(index) if item0.hasChildren(): self.setAllDelegatesPersistent(index0) else: - parentItem = self.modelActual.itemFromIndex(self.model().mapToSource(parentIndex)) + parentItem = self.modelActual.itemFromIndex( + self.model().mapToSource(parentIndex) + ) for i in range(parentItem.rowCount()): for column in self.delegateColumns: item = parentItem.child(i, column) item0 = parentItem.child(i, 0) - index = self.model().mapFromSource(self.modelActual.indexFromItem(item)) - index0 = self.model().mapFromSource(self.modelActual.indexFromItem(item0)) + index = self.model().mapFromSource( + self.modelActual.indexFromItem(item) + ) + index0 = self.model().mapFromSource( + self.modelActual.indexFromItem(item0) + ) if item0.showDelegate: self.openPersistentEditor(index) if item0.hasChildren(): @@ -634,7 +686,9 @@ def onCheckDelegate(self, item): sibling = self.modelActual.item(row, column) else: sibling = parent.child(row, column) - index = self.model().mapFromSource(self.modelActual.indexFromItem(sibling)) + index = self.model().mapFromSource( + self.modelActual.indexFromItem(sibling) + ) self.openPersistentEditor(index) self.scheduleDelayedItemsLayout() @@ -661,17 +715,17 @@ def onContextMenuRequested(self, pos): self.lastSelectedItem = item if item.star: - self.starItemAction.setText('un-star item') + self.starItemAction.setText("un-star item") self.starItemAction.setIcon(self.starCrossedIcon) else: - self.starItemAction.setText('star item') + self.starItemAction.setText("star item") self.starItemAction.setIcon(self.starIcon) if item.trash: - self.trashItemAction.setText('un-trash item') + self.trashItemAction.setText("un-trash item") self.trashItemAction.setIcon(self.trashCrossedIcon) else: - self.trashItemAction.setText('trash item') + self.trashItemAction.setText("trash item") self.trashItemAction.setIcon(self.trashIcon) self.contextMenu.exec_(self.mapToGlobal(pos)) @@ -690,24 +744,28 @@ class InstrumentDisplayBase(QtWidgets.QWidget): Basic widget. To implement new toolbars overload the makeToolBar function. To connect any extra signals overload the connectSignals function. All the type variables, require the class type and not an initialized object of the variables. - + :param instrument: The instrument we want to display the attribute from. :param attr: string of the name of the dictionary we want to display, like 'parameters' or 'function' - :param itemType: The type of item the model should use. + :param itemType: The type of item the model should use. :param modelType: The type of model that should be used. :param proxyModelType: The type of proxy model that should be used. :param viewType: The type of view that should be used. :param callSignals: If False, the constructor will not call the method connectSignals """ - def __init__(self, instrument, - attr: str, - itemType = ItemBase, - modelType = InstrumentModelBase, - proxyModelType = InstrumentSortFilterProxyModel, - viewType = InstrumentTreeViewBase, - callSignals: bool = True, - parent: Optional[QtWidgets.QWidget] = None, - **modelKwargs): + + def __init__( + self, + instrument, + attr: str, + itemType=ItemBase, + modelType=InstrumentModelBase, + proxyModelType=InstrumentSortFilterProxyModel, + viewType=InstrumentTreeViewBase, + callSignals: bool = True, + parent: Optional[QtWidgets.QWidget] = None, + **modelKwargs, + ): super().__init__(parent=parent) # initializing variables @@ -751,7 +809,9 @@ def connectSignals(self): self.lineEdit.textChanged.connect(self.proxyModel.onTextFilterChange) - self.view.header().sortIndicatorChanged.connect(self.proxyModel.onSortingIndicatorChanged) + self.view.header().sortIndicatorChanged.connect( + self.proxyModel.onSortingIndicatorChanged + ) def makeToolbar(self): """ @@ -783,15 +843,13 @@ def makeToolbar(self): toolbar.addSeparator() starAction = toolbar.addAction( - QtGui.QIcon(':/icons/star.svg'), - "Move Starred items to the top" + QtGui.QIcon(":/icons/star.svg"), "Move Starred items to the top" ) starAction.setCheckable(True) starAction.triggered.connect(lambda x: self.promoteStar()) trashAction = toolbar.addAction( - QtGui.QIcon(":/icons/trash-crossed.svg"), - "Hide trashed items" + QtGui.QIcon(":/icons/trash-crossed.svg"), "Hide trashed items" ) trashAction.setCheckable(True) trashAction.triggered.connect(lambda x: self.hideTrash()) @@ -828,16 +886,19 @@ def debuggingMethod(self): def fillChildren(parent): for i in range(parent.rowCount()): item = parent.child(i, 0) - items[item.name] = {'item': item, 'star': item.star, 'trash': item.trash} + items[item.name] = { + "item": item, + "star": item.star, + "trash": item.trash, + } if item.hasChildren(): fillChildren(item) for i in range(self.model.rowCount()): item = self.model.item(i, 0) - items[item.name] = {'item': item, 'star': item.star, 'trash': item.trash} + items[item.name] = {"item": item, "star": item.star, "trash": item.trash} if item.hasChildren(): fillChildren(item) pprint(items) print("\n \n \n \n") - diff --git a/src/instrumentserver/gui/instruments.py b/src/instrumentserver/gui/instruments.py index 0cf28de..e145d45 100644 --- a/src/instrumentserver/gui/instruments.py +++ b/src/instrumentserver/gui/instruments.py @@ -8,7 +8,13 @@ from qcodes import Parameter, Instrument from . import parameters, keepSmallHorizontally -from .base_instrument import InstrumentDisplayBase, ItemBase, InstrumentModelBase, InstrumentTreeViewBase, DelegateBase +from .base_instrument import ( + InstrumentDisplayBase, + ItemBase, + InstrumentModelBase, + InstrumentTreeViewBase, + DelegateBase, +) from .parameters import ParameterWidget, AnyInput, AnyInputForMethod from .. import QtWidgets, QtCore, QtGui, DEFAULT_PORT from ..blueprints import ParameterBroadcastBluePrint @@ -38,8 +44,9 @@ class AddParameterWidget(QtWidgets.QWidget): #: Signal(str) invalidParamRequested = QtCore.Signal(str) - def __init__(self, parent: Optional[QtWidgets.QWidget] = None, - typeInput: bool = False): + def __init__( + self, parent: Optional[QtWidgets.QWidget] = None, typeInput: bool = False + ): super().__init__(parent) self.typeInput = typeInput @@ -69,31 +76,34 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None, self.typeSelect = QtWidgets.QComboBox(self) names: list[str] = [] for t, v in parameterTypes.items(): - names.append(str(v['name'])) + names.append(str(v["name"])) for n in sorted(names): self.typeSelect.addItem(n) - self.typeSelect.setCurrentText(str(parameterTypes[ParameterTypes.numeric]['name'])) + self.typeSelect.setCurrentText( + str(parameterTypes[ParameterTypes.numeric]["name"]) + ) lbl = QtWidgets.QLabel("Type:") lbl.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) layout.addWidget(lbl, 1, 0) layout.addWidget(self.typeSelect, 1, 1) self.valsArgsEdit = QtWidgets.QLineEdit(self) - lbl = QtWidgets.QLabel('Type opts.:') - lbl.setToolTip("Optional, for constraining parameter values." - "Allowed args and defaults:\n" - " - 'Numeric': min_value=-1e18, max_value=1e18\n" - " - 'Integer': min_value=-inf, max_value=inf\n" - " - 'String': min_length=0, max_length=1e9\n" - 'See qcodes.utils.validators for details.') + lbl = QtWidgets.QLabel("Type opts.:") + lbl.setToolTip( + "Optional, for constraining parameter values." + "Allowed args and defaults:\n" + " - 'Numeric': min_value=-1e18, max_value=1e18\n" + " - 'Integer': min_value=-inf, max_value=inf\n" + " - 'String': min_length=0, max_length=1e9\n" + "See qcodes.utils.validators for details." + ) lbl.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) layout.addWidget(lbl, 1, 2) layout.addWidget(self.valsArgsEdit, 1, 3) self.addButton = QtWidgets.QPushButton( - QtGui.QIcon(":/icons/plus-square.svg"), - ' Add', - parent=self) + QtGui.QIcon(":/icons/plus-square.svg"), " Add", parent=self + ) self.addButton.clicked.connect(self.requestNewParameter) self.nameEdit.returnPressed.connect(self.addButton.click) @@ -103,9 +113,8 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None, self.addButton.setAutoDefault(True) self.clearButton = QtWidgets.QPushButton( - QtGui.QIcon(":/icons/delete.svg"), - ' Clear', - parent=self) + QtGui.QIcon(":/icons/delete.svg"), " Clear", parent=self + ) self.clearButton.setAutoDefault(True) self.clearButton.clicked.connect(self.clear) @@ -117,12 +126,14 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None, @QtCore.Slot() def clear(self): self.clearError() - self.nameEdit.setText('') - self.valueEdit.setText('') - self.unitEdit.setText('') + self.nameEdit.setText("") + self.valueEdit.setText("") + self.unitEdit.setText("") if self.typeInput: - self.typeSelect.setCurrentText(parameterTypes[ParameterTypes.numeric]['name']) - self.valsArgsEdit.setText('') + self.typeSelect.setCurrentText( + parameterTypes[ParameterTypes.numeric]["name"] + ) + self.valsArgsEdit.setText("") @QtCore.Slot(bool) def requestNewParameter(self, _): @@ -135,12 +146,12 @@ def requestNewParameter(self, _): value = self.valueEdit.text() unit = self.unitEdit.text() - if hasattr(self, 'typeSelect'): + if hasattr(self, "typeSelect"): ptype = paramTypeFromName(self.typeSelect.currentText()) valsArgs = self.valsArgsEdit.text() else: ptype = ParameterTypes.any - valsArgs = '' + valsArgs = "" self.newParamRequested.emit(name, value, unit, ptype, valsArgs) @@ -200,7 +211,7 @@ def runFun(self): if kwargs is not None: ret = self.fun(*args, **kwargs) else: - if isinstance(args, list) or isinstance(args, tuple) or args != '': + if isinstance(args, list) or isinstance(args, tuple) or args != "": ret = self.fun(*args) else: ret = self.fun() @@ -218,13 +229,14 @@ def getTooltipFromFun(cls, fun: Callable): """ sig = inspect.signature(fun) doc = inspect.getdoc(fun) - return str(sig) + '\n\n' + str(doc) + return str(sig) + "\n\n" + str(doc) # ----------------- Parameters Display Classes - Beginning ----------------------------- + class ItemParameters(ItemBase): - def __init__(self, unit='', **kwargs): + def __init__(self, unit="", **kwargs): super().__init__(**kwargs) self.unit = unit @@ -242,8 +254,12 @@ def __init__(self, parent=None): # used to keep a reference to the widget. self.parameters: Dict[str, QtWidgets.QWidget] = {} - def createEditor(self, widget: QtWidgets.QWidget, option: QtWidgets.QStyleOptionViewItem, - index: QtCore.QModelIndex) -> QtWidgets.QWidget: + def createEditor( + self, + widget: QtWidgets.QWidget, + option: QtWidgets.QStyleOptionViewItem, + index: QtCore.QModelIndex, + ) -> QtWidgets.QWidget: """ This is the function that is supposed to create the widget. It should return it. """ @@ -270,12 +286,14 @@ class ModelParameters(InstrumentModelBase): def __init__(self, *args, **kwargs): # make sure we pass the server ip and port properly to the subscriber when the values are not defaults. - subClientArgs = {"sub_host": kwargs.pop("sub_host", 'localhost'), - "sub_port": kwargs.pop("sub_port", DEFAULT_PORT + 1)} + subClientArgs = { + "sub_host": kwargs.pop("sub_host", "localhost"), + "sub_port": kwargs.pop("sub_port", DEFAULT_PORT + 1), + } super().__init__(*args, **kwargs) self.setColumnCount(3) - self.setHorizontalHeaderLabels([self.attr, 'unit', '']) + self.setHorizontalHeaderLabels([self.attr, "unit", ""]) # Live updates items self.cliThread = QtCore.QThread() @@ -298,26 +316,38 @@ def stopListener(self): @QtCore.Slot(ParameterBroadcastBluePrint) def updateParameter(self, bp: ParameterBroadcastBluePrint): - fullName = '.'.join(bp.name.split('.')[1:]) + fullName = ".".join(bp.name.split(".")[1:]) - if bp.action == 'parameter-creation': + if bp.action == "parameter-creation": if fullName not in self.instrument.list(): self.instrument.update() if fullName in self.instrument.list(): - self.addItem(fullName, element=nestedAttributeFromString(self.instrument, fullName)) + self.addItem( + fullName, + element=nestedAttributeFromString(self.instrument, fullName), + ) - elif bp.action == 'parameter-deletion': + elif bp.action == "parameter-deletion": self.removeItem(fullName) - elif bp.action == 'parameter-update' or bp.action == 'parameter-call': - item = self.findItems(fullName, QtCore.Qt.MatchExactly | QtCore.Qt.MatchRecursive, 0) + elif bp.action == "parameter-update" or bp.action == "parameter-call": + item = self.findItems( + fullName, QtCore.Qt.MatchExactly | QtCore.Qt.MatchRecursive, 0 + ) if len(item) == 0: if fullName not in self.itemsHide: try: - self.addItem(fullName, element=nestedAttributeFromString(self.instrument, fullName)) + self.addItem( + fullName, + element=nestedAttributeFromString( + self.instrument, fullName + ), + ) except AttributeError: # Parameter/submodule no longer exists (likely due to profile switch) - logger.debug(f"Ignoring broadcast for non-existent parameter: {fullName}") + logger.debug( + f"Ignoring broadcast for non-existent parameter: {fullName}" + ) else: assert isinstance(item[0], ItemBase) # The model can't actually modify the widget since it knows nothing about the view itself. @@ -326,7 +356,7 @@ def updateParameter(self, bp: ParameterBroadcastBluePrint): def insertItemTo(self, parent: QtGui.QStandardItem, item): if item is not None: # A parameter might not have a unit - unit = '' + unit = "" if item.element is not None: unit = item.element.unit unitItem = QtGui.QStandardItem(unit) @@ -359,35 +389,46 @@ def onItemNewValue(self, itemName, value): # use the abstract set method defined in parameter widget so it works for different types of widgets widget._setMethod(value) except RuntimeError as e: - logger.debug(f"Could not set value for {itemName} to {value}. Object is not being shown right now.") + logger.debug( + f"Could not set value for {itemName} to {value}. Object is not being shown right now." + ) class InstrumentParameters(InstrumentDisplayBase): - def __init__(self, instrument, parent=None, viewType=ParametersTreeView, callSignals: bool = True, **kwargs): - if 'instrument' in kwargs: - del kwargs['instrument'] + def __init__( + self, + instrument, + parent=None, + viewType=ParametersTreeView, + callSignals: bool = True, + **kwargs, + ): + if "instrument" in kwargs: + del kwargs["instrument"] modelKwargs = {} - if 'parameters-star' in kwargs: - modelKwargs['itemsStar'] = kwargs.pop('parameters-star') - if 'parameters-trash' in kwargs: - modelKwargs['itemsTrash'] = kwargs.pop('parameters-trash') - if 'parameters-hide' in kwargs: - modelKwargs['itemsHide'] = kwargs.pop('parameters-hide') + if "parameters-star" in kwargs: + modelKwargs["itemsStar"] = kwargs.pop("parameters-star") + if "parameters-trash" in kwargs: + modelKwargs["itemsTrash"] = kwargs.pop("parameters-trash") + if "parameters-hide" in kwargs: + modelKwargs["itemsHide"] = kwargs.pop("parameters-hide") # parameters for realtime update subscriber - if 'sub_host' in kwargs: - modelKwargs['sub_host'] = kwargs.pop('sub_host') - if 'sub_port' in kwargs: - modelKwargs['sub_port'] = kwargs.pop('sub_port') - - super().__init__(instrument=instrument, - parent=parent, - attr='parameters', - itemType=ItemParameters, - modelType=ModelParameters, - viewType=viewType, - callSignals=callSignals, - **modelKwargs) + if "sub_host" in kwargs: + modelKwargs["sub_host"] = kwargs.pop("sub_host") + if "sub_port" in kwargs: + modelKwargs["sub_port"] = kwargs.pop("sub_port") + + super().__init__( + instrument=instrument, + parent=parent, + attr="parameters", + itemType=ItemParameters, + modelType=ModelParameters, + viewType=viewType, + callSignals=callSignals, + **modelKwargs, + ) def connectSignals(self): super().connectSignals() @@ -404,8 +445,12 @@ class ParameterDeleteDelegate(ParameterDelegate): #: Emits the name of the parameter to be deleted when the user presses the delete button. removeParameter = QtCore.Signal(str) - def createEditor(self, widget: QtWidgets.QWidget, option: QtWidgets.QStyleOptionViewItem, - index: QtCore.QModelIndex) -> QtWidgets.QWidget: + def createEditor( + self, + widget: QtWidgets.QWidget, + option: QtWidgets.QStyleOptionViewItem, + index: QtCore.QModelIndex, + ) -> QtWidgets.QWidget: item = self.getItem(index) element = item.element rw = self.makeRemoveWidget(item.name, widget) @@ -416,8 +461,7 @@ def createEditor(self, widget: QtWidgets.QWidget, option: QtWidgets.QStyleOption return ret def makeRemoveWidget(self, fullName: str, widget: QtWidgets.QWidget): - w = QtWidgets.QPushButton( - QtGui.QIcon(":/icons/delete.svg"), "", parent=widget) + w = QtWidgets.QPushButton(QtGui.QIcon(":/icons/delete.svg"), "", parent=widget) w.setStyleSheet(""" QPushButton { background-color: salmon } """) @@ -445,7 +489,6 @@ def onItemNewValue(self, itemName, value): class ProfilesManager(QtWidgets.QComboBox): - #: Signal() #: Emitted when the selected index changed. indexChanged = QtCore.Signal() @@ -490,8 +533,19 @@ class ParameterManagerGui(InstrumentParameters): #: emitted when a parameter was created successfully parameterCreated = QtCore.Signal() - def __init__(self, instrument: Union[ProxyInstrument, ParameterManager], parent=None, **kwargs): - super().__init__(instrument, parent=None, viewType=ParameterManagerTreeView, callSignals=False, **kwargs) + def __init__( + self, + instrument: Union[ProxyInstrument, ParameterManager], + parent=None, + **kwargs, + ): + super().__init__( + instrument, + parent=None, + viewType=ParameterManagerTreeView, + callSignals=False, + **kwargs, + ) self.profileManager = ProfilesManager(parent=self) self.addParam = AddParameterWidget(parent=self) layout = self.layout() @@ -540,13 +594,16 @@ def removeParameter(self, fullName: str): def addParameter(self, fullName, value, unit): try: # Validators are commented out until they can be serialized. - self.instrument.add_parameter(fullName, initial_value=value, - unit=unit, ) # vals=vals) + self.instrument.add_parameter( + fullName, + initial_value=value, + unit=unit, + ) # vals=vals) self.parameterCreated.emit() except Exception as e: - self.parameterCreationError.emit(f"Could not create parameter." - f"Adding parameter raised" - f"{type(e)}: {e.args}") + self.parameterCreationError.emit( + f"Could not create parameter.Adding parameter raised{type(e)}: {e.args}" + ) return @QtCore.Slot() @@ -579,11 +636,10 @@ def saveToFile(self): class MethodsModel(InstrumentModelBase): - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setColumnCount(2) - self.setHorizontalHeaderLabels([self.attr, 'Arguments & Run']) + self.setHorizontalHeaderLabels([self.attr, "Arguments & Run"]) def insertItemTo(self, parent, item): if item is not None: @@ -600,20 +656,23 @@ def insertItemTo(self, parent, item): class MethodsDelegate(DelegateBase): - def __init__(self, parent=None): super().__init__(parent=parent) self.methods = {} - def createEditor(self, widget: QtWidgets.QWidget, option: QtWidgets.QStyleOptionViewItem, - index: QtCore.QModelIndex) -> QtWidgets.QWidget: + def createEditor( + self, + widget: QtWidgets.QWidget, + option: QtWidgets.QStyleOptionViewItem, + index: QtCore.QModelIndex, + ) -> QtWidgets.QWidget: item = self.getItem(index) element = item.element ret = MethodDisplay(element, item.name, parent=widget) parent = self.parent() - assert hasattr(parent, 'clearAlertsAction') + assert hasattr(parent, "clearAlertsAction") # connecting the widget with the clear alert signal parent.clearAlertsAction.triggered.connect(ret.alertLabel.clearAlert) @@ -626,7 +685,7 @@ def __init__(self, model, *args, **kwargs): super().__init__(model, [1], *args, **kwargs) # Adding the clear alert to the context menu - self.clearAlertsAction = QtWidgets.QAction('Clear alerts') + self.clearAlertsAction = QtWidgets.QAction("Clear alerts") self.contextMenu.addSeparator() self.contextMenu.addAction(self.clearAlertsAction) @@ -636,24 +695,25 @@ def __init__(self, model, *args, **kwargs): class InstrumentMethods(InstrumentDisplayBase): - def __init__(self, instrument, **kwargs): - if 'instrument' in kwargs: - del kwargs['instrument'] + if "instrument" in kwargs: + del kwargs["instrument"] modelKwargs = {} - if 'methods-star' in kwargs: - modelKwargs['itemsStar'] = kwargs.pop('methods-star') - if 'methods-trash' in kwargs: - modelKwargs['itemsTrash'] = kwargs.pop('methods-trash') - if 'methods-hide' in kwargs: - modelKwargs['itemsHide'] = kwargs.pop('methods-hide') - - super().__init__(instrument=instrument, - attr='functions', - modelType=MethodsModel, - viewType=MethodsTreeView, - **modelKwargs) + if "methods-star" in kwargs: + modelKwargs["itemsStar"] = kwargs.pop("methods-star") + if "methods-trash" in kwargs: + modelKwargs["itemsTrash"] = kwargs.pop("methods-trash") + if "methods-hide" in kwargs: + modelKwargs["itemsHide"] = kwargs.pop("methods-hide") + + super().__init__( + instrument=instrument, + attr="functions", + modelType=MethodsModel, + viewType=MethodsTreeView, + **modelKwargs, + ) # ----------------- Methods Display Classes - Ending ----------------------------------- @@ -664,22 +724,24 @@ class GenericInstrument(QtWidgets.QWidget): Widget that allows the display of real time parameters and changing their values. """ - def __init__(self, ins: Union[ProxyInstrument, Instrument], parent=None, **modelKwargs): + def __init__( + self, ins: Union[ProxyInstrument, Instrument], parent=None, **modelKwargs + ): super().__init__(parent=parent) self.ins = ins if type(ins) is ProxyInstrument: - inst_type = "Proxy-" + ins.bp.instrument_module_class.split('.')[-1] + inst_type = "Proxy-" + ins.bp.instrument_module_class.split(".")[-1] else: inst_type = ins.__class__.__name__ - - ins_label = f'{ins.name} | type: {inst_type}' + + ins_label = f"{ins.name} | type: {inst_type}" try: # added a unique device_id if the instrument has that method device_id = ins.device_id() - ins_label += f' | id: {device_id}' + ins_label += f" | id: {device_id}" except AttributeError: pass @@ -697,7 +759,6 @@ def __init__(self, ins: Union[ProxyInstrument, Instrument], parent=None, **model self._layout.addWidget(self.splitter) self.splitter.addWidget(self.parametersList) self.splitter.addWidget(self.methodsList) - # Resize param name, unit, and function name columns after entries loaded self.parametersList.view.resizeColumnToContents(0) @@ -706,7 +767,7 @@ def __init__(self, ins: Union[ProxyInstrument, Instrument], parent=None, **model def closeEvent(self, event: QtGui.QCloseEvent) -> None: """Stop the parameter subscriber thread before destruction.""" - model = getattr(self.parametersList, 'model', None) - if model is not None and hasattr(model, 'stopListener'): + model = getattr(self.parametersList, "model", None) + if model is not None and hasattr(model, "stopListener"): model.stopListener() super().closeEvent(event) diff --git a/src/instrumentserver/gui/misc.py b/src/instrumentserver/gui/misc.py index 3d6ee29..acb7cc4 100644 --- a/src/instrumentserver/gui/misc.py +++ b/src/instrumentserver/gui/misc.py @@ -4,15 +4,18 @@ class AlertLabel(QtWidgets.QLabel): - - def __init__(self, parent: Optional[QtWidgets.QWidget] = None, pixmapSize: Tuple[int, int] = (20, 20)): + def __init__( + self, + parent: Optional[QtWidgets.QWidget] = None, + pixmapSize: Tuple[int, int] = (20, 20), + ): super().__init__(parent) self.setAlignment(QtCore.Qt.AlignVCenter | QtCore.Qt.AlignHCenter) self._pixmapSize = pixmapSize pix = QtGui.QIcon(":/icons/no-alert.svg").pixmap(*pixmapSize) self.setPixmap(pix) - self.setToolTip('no alerts') + self.setToolTip("no alerts") @QtCore.Slot(str) def setAlert(self, message: str): @@ -24,13 +27,14 @@ def setAlert(self, message: str): def clearAlert(self): pix = QtGui.QIcon(":/icons/no-alert.svg").pixmap(*self._pixmapSize) self.setPixmap(pix) - self.setToolTip('no alerts') + self.setToolTip("no alerts") class AlertLabelGreen(AlertLabel): """ Expanding the functionality of the AlertLabel to add green alerts to indicate successful things """ + def mouseDoubleClickEvent(self, a0: QtGui.QMouseEvent) -> None: self.clearAlert() super().mouseDoubleClickEvent(a0) @@ -48,7 +52,6 @@ def setSuccssefulAlert(self, message: str): class DetachedTab(QtWidgets.QMainWindow): - #: Signal(QtWidgets.QWidget) #: emitted when a tab for the instrument is closed onCloseSignal = QtCore.Signal(object, str) @@ -70,7 +73,6 @@ def closeEvent(self, a0: QtGui.QCloseEvent) -> None: class SeparableTabBar(QtWidgets.QTabBar): - #: Signal(tabIndex, newPosition) #: Emitted when the user is dragging a tab out ofd the tab bar and should be detached. onDetachTab = QtCore.Signal(object, object) @@ -106,12 +108,14 @@ def mouseMoveEvent(self, a0: QtGui.QMouseEvent) -> None: Detects if the user is dragging a tab and starts the drag object. """ - if (a0.pos() - self.dragStartPos).manhattanLength() > QtWidgets.QApplication.startDragDistance() \ - and self.selectedIndex != -1: - + if ( + (a0.pos() - self.dragStartPos).manhattanLength() + > QtWidgets.QApplication.startDragDistance() + and self.selectedIndex != -1 + ): drag = QtGui.QDrag(self) mimeData = QtCore.QMimeData() - mimeData.setData('action', b'application/tab-detach') + mimeData.setData("action", b"application/tab-detach") drag.setMimeData(mimeData) pixmap = self.parentWidget().currentWidget().grab() # type: ignore[attr-defined] # I am pretty sure the stubs are wrong for this one, running through the debugger all the methods exists. @@ -131,7 +135,9 @@ def mouseMoveEvent(self, a0: QtGui.QMouseEvent) -> None: # A move action indicates that the user is trying to move the tabs around if dropAction == QtCore.Qt.MoveAction: a0.accept() - self.onMoveTab.emit(self.tabAt(self.dragStartPos), self.tabAt(self.dragDroppedPos)) + self.onMoveTab.emit( + self.tabAt(self.dragStartPos), self.tabAt(self.dragDroppedPos) + ) # An ignore action means that the user dropped the tab outside of the window and should be detached. elif dropAction == QtCore.Qt.IgnoreAction: @@ -145,7 +151,7 @@ def dragEnterEvent(self, a0: QtGui.QDragEnterEvent) -> None: mimeData = a0.mimeData() formats = mimeData.formats() - if 'action' in formats and mimeData.data('action') == 'application/tab-detach': + if "action" in formats and mimeData.data("action") == "application/tab-detach": a0.acceptProposedAction() super().dragMoveEvent(a0) @@ -183,10 +189,14 @@ def __init__(self, *args, **kwargs): def addUnclosableTab(self, widget, name): index = self.addTab(widget, name) - closeButton = self._tabBar.tabButton(index, QtWidgets.QTabBar.ButtonPosition.RightSide) + closeButton = self._tabBar.tabButton( + index, QtWidgets.QTabBar.ButtonPosition.RightSide + ) # on Mac the button is on the left side if closeButton is None: - closeButton = self._tabBar.tabButton(index, QtWidgets.QTabBar.ButtonPosition.LeftSide) + closeButton = self._tabBar.tabButton( + index, QtWidgets.QTabBar.ButtonPosition.LeftSide + ) closeButton.resize(0, 0) self.unclosableTabs[name] = widget @@ -223,7 +233,9 @@ def onMoveTab(self, fromIndex, toIndex): self.onCloseTab(fromIndex, True) self.insertTab(toIndex, widget, icon, text) if text in self.unclosableTabs: - self._tabBar.tabButton(toIndex, QtWidgets.QTabBar.ButtonPosition.RightSide).resize(0, 0) + self._tabBar.tabButton( + toIndex, QtWidgets.QTabBar.ButtonPosition.RightSide + ).resize(0, 0) self.setCurrentWidget(widget) @QtCore.Slot(int) @@ -253,7 +265,13 @@ class BaseDialog(QtWidgets.QDialog): :param tittleBarButtonsWidth: The width of pixels that the icon, the width will be set such that it is this number plus whatever the tittle is plus 15 extra pixels of margin. """ - def __init__(self, parent=None, flags=(QtCore.Qt.CustomizeWindowHint | QtCore.Qt.WindowCloseButtonHint), tittleBarButtonsWidth=108): + + def __init__( + self, + parent=None, + flags=(QtCore.Qt.CustomizeWindowHint | QtCore.Qt.WindowCloseButtonHint), + tittleBarButtonsWidth=108, + ): super().__init__(parent, flags=flags) self.tittleBarButtonsWidth = tittleBarButtonsWidth diff --git a/src/instrumentserver/gui/parameters.py b/src/instrumentserver/gui/parameters.py index 6f75f0e..94e3444 100644 --- a/src/instrumentserver/gui/parameters.py +++ b/src/instrumentserver/gui/parameters.py @@ -16,7 +16,8 @@ # TODO: do all styling with a global style sheet -FLOAT_PRECISION = 10 # The maximum number of significant digits for float numbers +FLOAT_PRECISION = 10 # The maximum number of significant digits for float numbers + def float_formater(val): """ @@ -49,8 +50,12 @@ class ParameterWidget(QtWidgets.QWidget): #: Signal(Any) -- _valueFromWidget = QtCore.Signal(object) - def __init__(self, parameter: Parameter, parent=None, - additionalWidgets: Optional[List[QtWidgets.QWidget]] = None): + def __init__( + self, + parameter: Parameter, + parent=None, + additionalWidgets: Optional[List[QtWidgets.QWidget]] = None, + ): super().__init__(parent) @@ -61,8 +66,9 @@ def __init__(self, parameter: Parameter, parent=None, self._setMethod = lambda x: None layout = QtWidgets.QGridLayout(self) - self.getButton = QtWidgets.QPushButton(QtGui.QIcon(":/icons/refresh.svg"), - "", parent=self) + self.getButton = QtWidgets.QPushButton( + QtGui.QIcon(":/icons/refresh.svg"), "", parent=self + ) self.getButton.pressed.connect(self.setWidgetFromParameter) keepSmallHorizontally(self.getButton) layout.addWidget(self.getButton, 0, 1) @@ -75,8 +81,7 @@ def __init__(self, parameter: Parameter, parent=None, layout.addWidget(self.alertWidget, 0, 3) # an input field will only be created if we have a set method. - if hasattr(parameter, 'set'): - + if hasattr(parameter, "set"): self.parameterSet.connect(lambda x: self.setButton.setPending(False)) self.parameterSet.connect(lambda x: self.alertWidget.clearAlert()) self.parameterPending.connect(lambda x: self.setButton.setPending(True)) @@ -87,7 +92,13 @@ def __init__(self, parameter: Parameter, parent=None, # input widget ptype = paramTypeFromVals(parameter.vals) vals = parameter.vals - self.paramWidget: NumberInput | AnyInput | QtWidgets.QLineEdit | QtWidgets.QCheckBox | QtWidgets.QLabel + self.paramWidget: ( + NumberInput + | AnyInput + | QtWidgets.QLineEdit + | QtWidgets.QCheckBox + | QtWidgets.QLabel + ) # FIXME: Currently blueprints don't pass validators meaning that we will never reach any of these if statements. # This should get uncommented when the blueprints are fixed. @@ -141,12 +152,17 @@ def __init__(self, parameter: Parameter, parent=None, else: self.setButton.setDisabled(True) self.paramWidget = QtWidgets.QLabel(self) - self._setMethod = lambda x: self.paramWidget.setText(str(x)) \ - if isinstance(self.paramWidget, QtWidgets.QLabel) else None - try: # also do immediate update for read-only params, as what we do for the editable parameters above. + self._setMethod = lambda x: ( + self.paramWidget.setText(str(x)) + if isinstance(self.paramWidget, QtWidgets.QLabel) + else None + ) + try: # also do immediate update for read-only params, as what we do for the editable parameters above. self._setMethod(parameter()) except Exception as e: - logger.warning(f"Error when setting parameter {parameter}: {e}", exc_info=True) + logger.warning( + f"Error when setting parameter {parameter}: {e}", exc_info=True + ) layout.addWidget(self.paramWidget, 0, 0) additionalWidgets = additionalWidgets or [] @@ -169,13 +185,13 @@ def onReturnPressed(self): self.paramWidget.input.deselect() self.setButton.setFocus() - def setParameter(self, value: Any): try: self._parameter.set(value) except Exception as e: - self.parameterSetError.emit(f"Could not set parameter, raised {type(e)}:" - f" {e.args}") + self.parameterSetError.emit( + f"Could not set parameter, raised {type(e)}: {e.args}" + ) return self.parameterSet.emit(value) @@ -206,12 +222,16 @@ def __init__(self, parent=None): self.input.textEdited.connect(self._processTextEdited) self.doEval = QtWidgets.QPushButton( - QtGui.QIcon(":/icons/python.svg"), "", parent=self, + QtGui.QIcon(":/icons/python.svg"), + "", + parent=self, ) self.doEval.setCheckable(True) self.doEval.setChecked(True) - self.doEval.setToolTip("Evaluate input as python expression.\n" - "If evaluation fails, treat as string.") + self.doEval.setToolTip( + "Evaluate input as python expression.\n" + "If evaluation fails, treat as string." + ) keepSmallHorizontally(self.doEval) layout = QtWidgets.QHBoxLayout(self) @@ -238,7 +258,9 @@ def setValue(self, val: Any): try: self.input.setText(float_formater(val)) except RuntimeError as e: - logger.debug(f"Could not set value {val} in AnyInput element does not exists, raised {type(e)}: {e.args}") + logger.debug( + f"Could not set value {val} in AnyInput element does not exists, raised {type(e)}: {e.args}" + ) @QtCore.Slot(str) def _processTextEdited(self, val: str): @@ -281,7 +303,10 @@ def setValue(self, value: numbers.Number): try: self.setText(float_formater(value)) except RuntimeError as e: - logger.debug(f"Could not set value {value} in NumberInput, raised {type(e)}: {e.args}") + logger.debug( + f"Could not set value {value} in NumberInput, raised {type(e)}: {e.args}" + ) + class AnyInputForMethod(AnyInput): """ @@ -292,17 +317,18 @@ class AnyInputForMethod(AnyInput): All arguments and keyword arguments are evaluated if the doEval button is checked, if not everything is treated like a long string. """ + def value(self): if self.doEval.isChecked(): # If '=' is present we need to separate the keyword from the value # If ',' is present we have more than one argument. - if '=' in self.input.text() or ',' in self.input.text(): - rawArgs = self.input.text().split(',') + if "=" in self.input.text() or "," in self.input.text(): + rawArgs = self.input.text().split(",") args = [] kwargs = {} for x in rawArgs: - if '=' in x: - key, value = x.split('=') + if "=" in x: + key, value = x.split("=") key = key.replace(" ", "") kwargs[key] = eval(value) else: @@ -315,7 +341,6 @@ def value(self): class SetButton(QtWidgets.QPushButton): - @QtCore.Slot(bool) def setPending(self, isPending: bool): if isPending: diff --git a/src/instrumentserver/helpers.py b/src/instrumentserver/helpers.py index dd2df0f..9ddc788 100644 --- a/src/instrumentserver/helpers.py +++ b/src/instrumentserver/helpers.py @@ -23,15 +23,15 @@ def stringToArgsAndKwargs(value: str) -> Tuple[List[Any], Dict[str, Any]]: - args or kwarg values cannot be evaluated with ``eval``. """ value = value.strip() - if value == '': + if value == "": return [], {} args = [] kwargs = {} - elts = [v.strip() for v in value.split(',')] + elts = [v.strip() for v in value.split(",")] for elt in elts: - if '=' in elt: - keyandval = elt.split('=') + if "=" in elt: + keyandval = elt.split("=") if len(keyandval) != 2: raise ValueError(f"{elt} cannot be interpreted as kwarg") try: @@ -62,7 +62,7 @@ def nestedAttributeFromString(root: Any, loc: str) -> Any: returns the object that can be found at parent_object.foo.bar.spam.bacon. """ - mods = loc.split('.') + mods = loc.split(".") obj = root for m in mods: obj = getattr(obj, m) @@ -76,13 +76,15 @@ def getInstrumentParameters(ins: Instrument) -> Dict[str, Dict[str, str]]: :returns: a param dict with entries `unit`, `vals`, for each instrument parameter. """ - paramDict = toParamDict([ins], includeMeta=['unit', 'vals']) + paramDict = toParamDict([ins], includeMeta=["unit", "vals"]) for k, v in paramDict.items(): - paramDict[k].pop('value', None) + paramDict[k].pop("value", None) return paramDict -def getInstrumentMethods(ins: Instrument) -> Dict[str, Dict[str, Union[str, List[str]]]]: +def getInstrumentMethods( + ins: Instrument, +) -> Dict[str, Dict[str, Union[str, List[str]]]]: """Return the methods of an instrument. :param ins: instrument instance @@ -94,7 +96,7 @@ def getInstrumentMethods(ins: Instrument) -> Dict[str, Dict[str, Union[str, List """ funcs: dict = {} for attr_name in dir(ins): - if attr_name[0] != '_' and attr_name not in dir(Instrument): + if attr_name[0] != "_" and attr_name not in dir(Instrument): obj = getattr(ins, attr_name) if callable(obj) and not isinstance(obj, Parameter): funcs[attr_name] = dict() @@ -102,10 +104,11 @@ def getInstrumentMethods(ins: Instrument) -> Dict[str, Dict[str, Union[str, List for fname in funcs.keys(): fun = getattr(ins, fname) signature = inspect.signature(fun) - funcs[fname]['parameters'] = [str(signature.parameters[a]) for a in - signature.parameters] - funcs[fname]['doc'] = str(fun.__doc__) - funcs[fname]['return'] = str(signature.return_annotation) + funcs[fname]["parameters"] = [ + str(signature.parameters[a]) for a in signature.parameters + ] + funcs[fname]["doc"] = str(fun.__doc__) + funcs[fname]["return"] = str(signature.return_annotation) return funcs @@ -134,7 +137,7 @@ def flat_to_nested_dict(flat_dict: Dict) -> Dict: """ nested = {} for key, value in flat_dict.items(): - parts = key.split('.') + parts = key.split(".") d = nested for part in parts[:-1]: d = d.setdefault(part, {}) @@ -142,13 +145,14 @@ def flat_to_nested_dict(flat_dict: Dict) -> Dict: return nested -def is_flat_dict(d:dict) -> bool: +def is_flat_dict(d: dict) -> bool: """ Detects if a dictionary is flat (i.e. all values are non-dicts). """ return all(not isinstance(v, dict) for v in d.values()) -def flatten_dict(d, sep='.'): + +def flatten_dict(d, sep="."): """ Detects if a dictionary is flat (i.e. all values are non-dicts). If it is not flat, recursively flattens it using dot-separated keys. @@ -171,7 +175,7 @@ def flatten_dict(d, sep='.'): if is_flat_dict(d): return d - def flatten(nested, parent_key=''): + def flatten(nested, parent_key=""): items = {} for k, v in nested.items(): new_key = f"{parent_key}{sep}{k}" if parent_key else k @@ -181,4 +185,4 @@ def flatten(nested, parent_key=''): items[new_key] = v return items - return flatten(d) \ No newline at end of file + return flatten(d) diff --git a/src/instrumentserver/log.py b/src/instrumentserver/log.py index edee369..738df2c 100644 --- a/src/instrumentserver/log.py +++ b/src/instrumentserver/log.py @@ -19,16 +19,16 @@ class LogLevels(Enum): debug = auto() -class QLogHandler(QtCore.QObject,logging.Handler): +class QLogHandler(QtCore.QObject, logging.Handler): """A simple log handler that supports logging in TextEdit""" COLORS = { - logging.ERROR: QtGui.QColor('red'), - logging.WARNING: QtGui.QColor('orange'), - logging.INFO: QtGui.QColor('green'), - logging.DEBUG: QtGui.QColor('gray'), + logging.ERROR: QtGui.QColor("red"), + logging.WARNING: QtGui.QColor("orange"), + logging.INFO: QtGui.QColor("green"), + logging.DEBUG: QtGui.QColor("gray"), } - + new_html = QtCore.Signal(str) def __init__(self, parent): @@ -41,8 +41,7 @@ def __init__(self, parent): # connect signal to slot that actually touches the widget (GUI thread) self.new_html.connect(self._append_html) - - + @QtCore.Slot(str) def _append_html(self, html: str): """Append HTML to the text widget in the GUI thread.""" @@ -54,7 +53,6 @@ def _append_html(self, html: str): self.widget.verticalScrollBar().maximum() ) - def set_transform(self, fn): """fn(record, msg) -> str | {'html': str} | None""" self._transform = fn @@ -65,7 +63,7 @@ def emit(self, record): raw_msg = record.getMessage() # message only # Color for prefix (log level) - clr = self.COLORS.get(record.levelno, QtGui.QColor('black')).name() + clr = self.COLORS.get(record.levelno, QtGui.QColor("black")).name() if self._transform is not None: html_fragment = self._transform(record, raw_msg) @@ -73,7 +71,7 @@ def emit(self, record): i = formatted.rfind(raw_msg) if i >= 0: prefix = formatted[:i] - suffix = formatted[i + len(raw_msg):] + suffix = formatted[i + len(raw_msg) :] else: prefix, suffix = "", "" @@ -90,29 +88,33 @@ def emit(self, record): # fallback: original plain text path msg = formatted - clr_q = self.COLORS.get(record.levelno, QtGui.QColor('black')).name() + clr_q = self.COLORS.get(record.levelno, QtGui.QColor("black")).name() html = f"{escape(msg)}" self.new_html.emit(html) except RuntimeError: # Widget has been destroyed; detach self from the logger so we # stop receiving further records and Python can collect us. - for lg in list(logging.Logger.manager.loggerDict.values()) + [logging.getLogger()]: + for lg in list(logging.Logger.manager.loggerDict.values()) + [ + logging.getLogger() + ]: if isinstance(lg, logging.Logger) and self in lg.handlers: lg.removeHandler(self) + class LogWidget(QtWidgets.QWidget): """ A simple logger widget. Uses QLogHandler as handler. The handler has the actual widget that is used to display the logs. """ + def __init__(self, parent=None, level=logging.INFO): super().__init__(parent) # set up the graphical handler fmt = logging.Formatter( "[%(asctime)s] [%(name)s: %(levelname)s] %(message)s", - datefmt='%m-%d %H:%M:%S', + datefmt="%m-%d %H:%M:%S", ) logTextBox = QLogHandler(self) logTextBox.setFormatter(fmt) @@ -126,7 +128,7 @@ def __init__(self, parent=None, level=logging.INFO): self.setLayout(layout) # configure the logger - self.logger = logging.getLogger('instrumentserver') + self.logger = logging.getLogger("instrumentserver") # delete old graphical handler. however, that would allow only one # graphical handler per kernel. not sure i want that...? @@ -145,10 +147,12 @@ def _param_update_formatter(record, raw_msg): A formater that makes parameter updates more prominent in the gui log window. """ # Pattern 1: "parameter-update" from the broadcaster, for client station - pattern_update = re.compile(r'parameter-update:\s*([A-Za-z0-9_.]+):\s*(.+)', re.S) + pattern_update = re.compile(r"parameter-update:\s*([A-Za-z0-9_.]+):\s*(.+)", re.S) # Pattern 2: normal log message from the server. i.e. `Parameter {name} set to: {value}` - pattern_info = re.compile(r"Parameter\s+'([A-Za-z0-9_.]+)'\s+set\s+to:\s*(.+)", re.S) + pattern_info = re.compile( + r"Parameter\s+'([A-Za-z0-9_.]+)'\s+set\s+to:\s*(.+)", re.S + ) match = pattern_update.search(raw_msg) or pattern_info.search(raw_msg) if not match: @@ -157,12 +161,18 @@ def _param_update_formatter(record, raw_msg): name, value = match.groups() # Escape HTML but keep \n literal (QTextEdit.append will render them) - return ( f"{escape(name)} set to: " f"{escape(value)}" ) - - -def setupLogging(addStreamHandler=True, logFile=None, - name='instrumentserver', - streamHandlerLevel=logging.INFO): + return ( + f"{escape(name)} set to: " + f"{escape(value)}" + ) + + +def setupLogging( + addStreamHandler=True, + logFile=None, + name="instrumentserver", + streamHandlerLevel=logging.INFO, +): """Setting up logging, including adding a custom handler.""" logger = logging.getLogger(name) @@ -174,7 +184,7 @@ def setupLogging(addStreamHandler=True, logFile=None, if logFile is not None: fmt = logging.Formatter( "%(asctime)s\t: %(name)s\t: %(levelname)s\t: %(message)s", - datefmt='%Y-%m-%d %H:%M:%S', + datefmt="%Y-%m-%d %H:%M:%S", ) fh = logging.FileHandler(logFile) fh.setFormatter(fmt) @@ -184,7 +194,7 @@ def setupLogging(addStreamHandler=True, logFile=None, if addStreamHandler: fmt = logging.Formatter( "[%(asctime)s] [%(name)s: %(levelname)s] %(message)s", - datefmt='%m/%d %H:%M', + datefmt="%m/%d %H:%M", ) streamHandler = logging.StreamHandler(sys.stderr) streamHandler.setFormatter(fmt) @@ -194,7 +204,7 @@ def setupLogging(addStreamHandler=True, logFile=None, logger.info(f"Logging set up for {name}.") -def logger(name='instrumentserver'): +def logger(name="instrumentserver"): """Get the (root) logger for the package.""" return logging.getLogger(name) diff --git a/src/instrumentserver/monitoring/listener.py b/src/instrumentserver/monitoring/listener.py index 94667e5..04ec185 100644 --- a/src/instrumentserver/monitoring/listener.py +++ b/src/instrumentserver/monitoring/listener.py @@ -11,6 +11,7 @@ import pandas as pd import ruamel.yaml # type: ignore[import-untyped] # Known bugfix under no-fix status: https://sourceforge.net/p/ruamel-yaml/tickets/328/ import zmq + try: from influxdb_client import InfluxDBClient, Point, WriteOptions except ImportError: @@ -22,9 +23,10 @@ logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) + class Listener(ABC): def __init__(self, addresses: list): - self.addresses = addresses + self.addresses = addresses def run(self): @@ -49,7 +51,7 @@ def run(self): try: # parses string message and decodes into ParameterBroadcastBluePrint message = recvMultipart(socket) - self.listenerEvent(message[0],message[1]) + self.listenerEvent(message[0], message[1]) except (KeyboardInterrupt, SystemExit): # exit if keyboard interrupt logger.info("Program Stopped Manually") @@ -67,32 +69,33 @@ class CSVConfig: @classmethod def from_dict(cls, config_dict): return cls( - addresses=config_dict['addresses'], - params=config_dict['params'], - csv_path=config_dict['csv_path'] + addresses=config_dict["addresses"], + params=config_dict["params"], + csv_path=config_dict["csv_path"], ) - + + @dataclass class InfluxConfig: addresses: list params: list token: str org: str - bucketDict: Dict[str,str] + bucketDict: Dict[str, str] url: str - measurementNameDict: Dict[str,str] - timezone_name: str = 'CDT' + measurementNameDict: Dict[str, str] + timezone_name: str = "CDT" @classmethod def from_dict(cls, config_dict): return cls( - addresses=config_dict['addresses'], - params=config_dict['params'], - token=config_dict['token'], - org=config_dict['org'], - bucketDict=config_dict['bucketDict'], - url=config_dict['url'], - measurementNameDict=config_dict['measurementNameDict'] + addresses=config_dict["addresses"], + params=config_dict["params"], + token=config_dict["token"], + org=config_dict["org"], + bucketDict=config_dict["bucketDict"], + url=config_dict["url"], + measurementNameDict=config_dict["measurementNameDict"], ) @@ -108,7 +111,7 @@ def __init__(self, csvConfig: CSVConfig): self.df = pd.read_csv(self.path) self.df = self.df.drop("Unnamed: 0", axis=1) else: - self.df = pd.DataFrame(columns=["time","name","value","unit"]) + self.df = pd.DataFrame(columns=["time", "name", "value", "unit"]) self.paramList = list(csvConfig.params) @@ -116,19 +119,29 @@ def run(self): super().run() def listenerEvent(self, message: ParameterBroadcastBluePrint): - + # listens only for parameters in the list, if it is empty, it listens to everything if not self.paramList: logger.info(f"Writing data [{message.name},{message.value},{message.unit}]") - self.df.loc[len(self.df)]=[datetime.now(),message.name,message.value,message.unit] + self.df.loc[len(self.df)] = [ + datetime.now(), + message.name, + message.value, + message.unit, + ] self.df.to_csv(self.path) elif message.name in self.paramList: logger.info(f"Writing data [{message.name},{message.value},{message.unit}]") - self.df.loc[len(self.df)]=[datetime.now(),message.name,message.value,message.unit] + self.df.loc[len(self.df)] = [ + datetime.now(), + message.name, + message.value, + message.unit, + ] self.df.to_csv(self.path) -class InfluxListener(Listener): +class InfluxListener(Listener): def __init__(self, influxConfig: InfluxConfig): super().__init__(influxConfig.addresses) @@ -166,15 +179,24 @@ def listenerEvent(self, instrument, message: ParameterBroadcastBluePrint): def checkInfluxConfig(configInput: Dict[str, Any]): # check if all fields are present in the config file - influxFields = ['addresses', 'params', 'token', 'org', 'bucketDict', 'url', 'measurementNameDict'] + influxFields = [ + "addresses", + "params", + "token", + "org", + "bucketDict", + "url", + "measurementNameDict", + ] for field in influxFields: if field not in configInput or configInput[field] is None: logger.info(f"Missing field {field} in config file") return False - if 'measurementName' not in configInput or configInput['measurementName'] is None: - configInput['measurementName'] = 'my_measurement' + if "measurementName" not in configInput or configInput["measurementName"] is None: + configInput["measurementName"] = "my_measurement" return True + def checkCSVConfig(configInput: Dict[str, Any]): # check if all fields are present in the config file @@ -185,17 +207,18 @@ def checkCSVConfig(configInput: Dict[str, Any]): return False return True + def get_timezone_info(timezone_name): try: return ZoneInfo(timezone_name) except ZoneInfoNotFoundError: print(f"Unknown timezone: {timezone_name}") return None - + def startListener(): - parser = argparse.ArgumentParser(description='Starting the listener') + parser = argparse.ArgumentParser(description="Starting the listener") parser.add_argument("-c", "--config") args = parser.parse_args() @@ -203,23 +226,23 @@ def startListener(): yaml = ruamel.yaml.YAML() # load variables from config file - if configPath != '' and configPath is not None: + if configPath != "" and configPath is not None: configInput = yaml.load(configPath) else: logger.warning("Please enter a valid path for the config file") return 0 # start listener that writes to CSV or Influx Database - if 'type' in configInput: - if configInput['type'] == "CSV": + if "type" in configInput: + if configInput["type"] == "CSV": if checkCSVConfig(configInput): CSVListener = DFListener(CSVConfig.from_dict(configInput)) CSVListener.run() - elif configInput['type'] == "Influx": + elif configInput["type"] == "Influx": if checkInfluxConfig(configInput): DBListener = InfluxListener(InfluxConfig.from_dict(configInput)) DBListener.run() else: logger.warning(f"Type {configInput['type']} not supported") else: - logger.warning("Please enter a valid type in the config file") \ No newline at end of file + logger.warning("Please enter a valid type in the config file") diff --git a/src/instrumentserver/params.py b/src/instrumentserver/params.py index 07de93f..4830b08 100644 --- a/src/instrumentserver/params.py +++ b/src/instrumentserver/params.py @@ -27,24 +27,15 @@ class ParameterTypes(Enum): parameterTypes = { - ParameterTypes.any: - {'name': 'Any', - 'validatorType': validators.Anything}, - ParameterTypes.numeric: - {'name': 'Numeric', - 'validatorType': validators.Numbers}, - ParameterTypes.integer: - {'name': 'Integer', - 'validatorType': validators.Ints}, - ParameterTypes.string: - {'name': 'String', - 'validatorType': validators.Strings}, - ParameterTypes.bool: - {'name': 'Boolean', - 'validatorType': validators.Bool}, - ParameterTypes.complex: - {'name': 'Complex', - 'validatorType': validators.ComplexNumbers}, + ParameterTypes.any: {"name": "Any", "validatorType": validators.Anything}, + ParameterTypes.numeric: {"name": "Numeric", "validatorType": validators.Numbers}, + ParameterTypes.integer: {"name": "Integer", "validatorType": validators.Ints}, + ParameterTypes.string: {"name": "String", "validatorType": validators.Strings}, + ParameterTypes.bool: {"name": "Boolean", "validatorType": validators.Bool}, + ParameterTypes.complex: { + "name": "Complex", + "validatorType": validators.ComplexNumbers, + }, } @@ -53,7 +44,7 @@ def paramTypeFromVals(vals: validators.Validator | None) -> Union[ParameterTypes vals = validators.Anything() for k, v in parameterTypes.items(): - validator_type = v['validatorType'] + validator_type = v["validatorType"] if isinstance(validator_type, type) and isinstance(vals, validator_type): return k @@ -62,7 +53,7 @@ def paramTypeFromVals(vals: validators.Validator | None) -> Union[ParameterTypes def paramTypeFromName(name: str) -> Union[ParameterTypes, None]: for k, v in parameterTypes.items(): - if name == v['name']: + if name == v["name"]: return k return None @@ -123,7 +114,7 @@ def cleanProfileName(name: str) -> str: When passed the full file name of a parameter_manager profile, return only the middle string representing the profile's name. """ - return name.replace('parameter_manager-', '').replace('.json', '') + return name.replace("parameter_manager-", "").replace(".json", "") @staticmethod def fullProfileName(name: str) -> str: @@ -131,14 +122,14 @@ def fullProfileName(name: str) -> str: Adds 'parameter_manager-' to the beginning of `name` and adds '.json' at the end. """ - if not name.startswith('parameter_manager-'): - name = 'parameter_manager-' + name - if not name.endswith('.json'): - name += '.json' + if not name.startswith("parameter_manager-"): + name = "parameter_manager-" + name + if not name.endswith(".json"): + name += ".json" return name @classmethod - def _to_tree(cls, pm: 'ParameterManager') -> Dict: + def _to_tree(cls, pm: "ParameterManager") -> Dict: ret: dict[str, Any] = {} for smn, sm in pm.submodules.items(): assert isinstance(sm, ParameterManager) @@ -176,27 +167,30 @@ def to_tree(self): def _get_param(self, param_name: str) -> ParameterBase: parent = self._get_parent(param_name) try: - param = parent.parameters[param_name.split('.')[-1]] + param = parent.parameters[param_name.split(".")[-1]] return param except KeyError: raise ValueError(f"Parameter '{param_name}' does not exist") - def _get_parent(self, param_name: str, create_parent: bool = False) \ - -> 'ParameterManager': + def _get_parent( + self, param_name: str, create_parent: bool = False + ) -> "ParameterManager": - split_names = param_name.split('.') + split_names = param_name.split(".") parent = self full_name = self.name for i, n in enumerate(split_names[:-1]): - full_name += f'.{n}' + full_name += f".{n}" if n in parent.parameters: - raise ValueError(f"{n} is a parameter, and cannot have child parameters.") + raise ValueError( + f"{n} is a parameter, and cannot have child parameters." + ) if n not in parent.submodules: if create_parent: parent.add_submodule(n, ParameterManager(n)) # type: ignore # This one is technically breaking the type hints from qcodes itself, but it seems to work fine. else: - raise ValueError(f'{n} does not exist.') + raise ValueError(f"{n} does not exist.") parent = parent.submodules[n] # type: ignore # This one is technically breaking the type hints from qcodes itself, but it seems to work fine. return parent @@ -223,20 +217,20 @@ def add_parameter(self, name: str, **kw: Any) -> None: # type: ignore # Breaks - ``vals`` defaults to ``qcodes.utils.validators.Anything()``. :return: None. """ - kw['parameter_class'] = Parameter - if 'vals' not in kw: - kw['vals'] = validators.Anything() - kw['set_cmd'] = None + kw["parameter_class"] = Parameter + if "vals" not in kw: + kw["vals"] = validators.Anything() + kw["set_cmd"] = None parent = self._get_parent(name, create_parent=True) if parent is self: - super().add_parameter(name.split('.')[-1], **kw) + super().add_parameter(name.split(".")[-1], **kw) else: - parent.add_parameter(name.split('.')[-1], **kw) + parent.add_parameter(name.split(".")[-1], **kw) def remove_parameter(self, param_name: str, cleanup: bool = True): parent = self._get_parent(param_name) - pname = param_name.split('.')[-1] + pname = param_name.split(".")[-1] del parent.parameters[pname] if cleanup: self.remove_empty_submodules() @@ -311,16 +305,20 @@ def fromFile(self, filePath: str | None = None, deleteMissing: bool = True): ParameterManager that are not listed in the file. """ if filePath is None: - filePath = self.workingDirectory.joinpath(self.fullProfileName(self.selectedProfile)) + filePath = self.workingDirectory.joinpath( + self.fullProfileName(self.selectedProfile) + ) if os.path.exists(filePath): - with open(filePath, 'r') as f: + with open(filePath, "r") as f: pd = json.load(f) self.fromParamDict(pd) path = Path(filePath) - if path.name.startswith("parameter_manager-") and path.name.endswith(".json"): + if path.name.startswith("parameter_manager-") and path.name.endswith( + ".json" + ): profileName = path.name self.selectedProfile = profileName if path.name not in self.profiles: @@ -329,8 +327,7 @@ def fromFile(self, filePath: str | None = None, deleteMissing: bool = True): else: logger.warning("parameter file not found, cannot load.") - def fromParamDict(self, paramDict: Dict[str, Any], - deleteMissing: bool = True): + def fromParamDict(self, paramDict: Dict[str, Any], deleteMissing: bool = True): """Load parameters from a parameter dictionary (see :mod:`.serialize`). :param paramDict: Parameter dictionary. @@ -344,16 +341,19 @@ def fromParamDict(self, paramDict: Dict[str, Any], simple = False currentParams = self.list() - fileParams = ['.'.join(k.split('.')[1:]) for k in paramDict.keys() - if k.split('.')[0] == self.name] + fileParams = [ + ".".join(k.split(".")[1:]) + for k in paramDict.keys() + if k.split(".")[0] == self.name + ] for pn in fileParams: if simple: val = paramDict[f"{self.name}.{pn}"] - unit = '' + unit = "" else: - val = paramDict[f"{self.name}.{pn}"]['value'] - unit = paramDict[f"{self.name}.{pn}"].get('unit', '') + val = paramDict[f"{self.name}.{pn}"]["value"] + unit = paramDict[f"{self.name}.{pn}"].get("unit", "") if self.has_param(pn): self.parameter(pn)(val) @@ -369,13 +369,15 @@ def fromParamDict(self, paramDict: Dict[str, Any], if pn not in fileParams and deleteMissing: self.remove_parameter(pn) - def toParamDict(self, simpleFormat: bool = False, includeMeta: List[str] = ['unit']): - params = serialize.toParamDict([self], simpleFormat=simpleFormat, - includeMeta=includeMeta) + def toParamDict( + self, simpleFormat: bool = False, includeMeta: List[str] = ["unit"] + ): + params = serialize.toParamDict( + [self], simpleFormat=simpleFormat, includeMeta=includeMeta + ) return params def toFile(self, filePath: str | None = None, name: str | None = None): - """Save parameters from the instrument into a json file. If the file being saved is a profile file (starts with 'parameter_manager-' and ends with '.json'), the selectedProfile is changed to the filename. @@ -395,13 +397,13 @@ def toFile(self, filePath: str | None = None, name: str | None = None): if os.path.isdir(filePath): if name is None: name = self.selectedProfile - filePath = os.path.join(filePath, self.fullProfileName(name)) + filePath = os.path.join(filePath, self.fullProfileName(name)) folder, file = os.path.split(filePath) params = self.toParamDict() if not os.path.exists(folder): os.makedirs(folder) - with open(filePath, 'w') as f: + with open(filePath, "w") as f: json.dump(params, f, indent=2, sort_keys=True) file = str(file) @@ -423,5 +425,7 @@ def switch_to_profile(self, profile: str): self.toFile(str(self.workingDirectory), self.selectedProfile) self.remove_all_parameters() - self.fromFile(str(self.workingDirectory.joinpath(self.fullProfileName(profile)))) + self.fromFile( + str(self.workingDirectory.joinpath(self.fullProfileName(profile))) + ) self.selectedProfile = self.fullProfileName(profile) diff --git a/src/instrumentserver/resource.py b/src/instrumentserver/resource.py index b8f97f0..d4fd9e9 100644 --- a/src/instrumentserver/resource.py +++ b/src/instrumentserver/resource.py @@ -1456,7 +1456,7 @@ \x00\x00\x01\x95\xe8\xaa\xa9\xd4\ " -qt_version = [int(v) for v in QtCore.qVersion().split('.')] +qt_version = [int(v) for v in QtCore.qVersion().split(".")] if qt_version < [5, 8, 0]: rcc_version = 1 qt_resource_struct = qt_resource_struct_v1 @@ -1464,10 +1464,17 @@ rcc_version = 2 qt_resource_struct = qt_resource_struct_v2 + def qInitResources(): - QtCore.qRegisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) + QtCore.qRegisterResourceData( + rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data + ) + def qCleanupResources(): - QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) + QtCore.qUnregisterResourceData( + rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data + ) + qInitResources() diff --git a/src/instrumentserver/serialize.py b/src/instrumentserver/serialize.py index 0f7bc98..7934894 100644 --- a/src/instrumentserver/serialize.py +++ b/src/instrumentserver/serialize.py @@ -85,11 +85,13 @@ SerializableType = Union[Station, List[Union[InstrumentBase, Parameter]]] -def toParamDict(input: SerializableType, - get: bool = False, - includeMeta: List[str] = [], - excludeParameters: List[str] = [], - simpleFormat: bool = True) -> Dict: +def toParamDict( + input: SerializableType, + get: bool = False, + includeMeta: List[str] = [], + excludeParameters: List[str] = [], + simpleFormat: bool = True, +) -> Dict: """Create a dictionary that holds parameter values, and optionally additional information about them. @@ -113,33 +115,43 @@ def toParamDict(input: SerializableType, snap = input.get_snapshot() else: snap = input.snapshot() - input = [getattr(input, k) for k in snap['instruments'].keys()] \ - + [getattr(input, k) for k in snap['parameters'].keys()] \ - + [getattr(input, k) for k in snap['components'].keys()] + input = ( + [getattr(input, k) for k in snap["instruments"].keys()] + + [getattr(input, k) for k in snap["parameters"].keys()] + + [getattr(input, k) for k in snap["components"].keys()] + ) ret = {} for obj in input: if isinstance(obj, InstrumentBase): - ret.update(_singleInstrumentParametersToJson( - obj, get=get, addPrefix=f"{obj.name}.", - includeMeta=includeMeta, - simpleFormat=simpleFormat, - excludeParameters=excludeParameters)) + ret.update( + _singleInstrumentParametersToJson( + obj, + get=get, + addPrefix=f"{obj.name}.", + includeMeta=includeMeta, + simpleFormat=simpleFormat, + excludeParameters=excludeParameters, + ) + ) elif isinstance(obj, Parameter): - ret.update(_singleParameterToJson( - obj, get=get, simpleFormat=simpleFormat, - includeMeta=includeMeta)) + ret.update( + _singleParameterToJson( + obj, get=get, simpleFormat=simpleFormat, includeMeta=includeMeta + ) + ) else: - raise ValueError(f"Invalid object: {obj}. Can only process " - f"Station, Instrument, and Parameter.") + raise ValueError( + f"Invalid object: {obj}. Can only process " + f"Station, Instrument, and Parameter." + ) return ret -def fromParamDict(paramDict: Dict[str, Any], - target: SerializableType) -> None: +def fromParamDict(paramDict: Dict[str, Any], target: SerializableType) -> None: """Load parameter values from JSON. :param paramDict: The parameter dictionary in a valid JSON format (may be @@ -151,7 +163,7 @@ def fromParamDict(paramDict: Dict[str, Any], simple = isSimpleFormat(paramDict) for k in sorted(paramDict.keys()): - paramAsList = k.split('.') + paramAsList = k.split(".") parent = _getObjectByName(paramAsList[0], src=target) if parent is None: @@ -167,9 +179,9 @@ def fromParamDict(paramDict: Dict[str, Any], if simple: value = paramDict[k] else: - value = paramDict[k]['value'] + value = paramDict[k]["value"] - if hasattr(param, 'set'): + if hasattr(param, "set"): logger.info(f"[{k}] set to: {value}") try: param.set(value) @@ -178,8 +190,10 @@ def fromParamDict(paramDict: Dict[str, Any], else: logger.info(f"[{k}] does not support setting, ignore.") + # Tools + def isSimpleFormat(paramDict: Dict[str, Any]): """Checks if the supplied paramDict is in the simplified format. @@ -208,16 +222,19 @@ def validateParamDict(params: Dict[str, Any]): def toDataFrame(input: SerializableType): """Make a pandas data frame from the parameters. Mainly useful for printing overviews in notebooks.""" - params = toParamDict(input, includeMeta=['unit', 'vals']) + params = toParamDict(input, includeMeta=["unit", "vals"]) return pd.DataFrame(params).T.sort_index() # private tool functions -def _singleParameterToJson(parameter: Parameter, - get: bool = False, - includeMeta: List[str] = [], - simpleFormat: bool = True) -> Dict: + +def _singleParameterToJson( + parameter: Parameter, + get: bool = False, + includeMeta: List[str] = [], + simpleFormat: bool = True, +) -> Dict: """Create a JSON representation of a parameter.""" ret: dict[str, Any] = {parameter.name: None} @@ -226,21 +243,23 @@ def _singleParameterToJson(parameter: Parameter, else: snap = parameter.snapshot(update=get) if len(includeMeta) == 0 and simpleFormat: - ret[parameter.name] = snap.get('value', None) + ret[parameter.name] = snap.get("value", None) else: ret[parameter.name] = dict() - for k in ['value'] + includeMeta: + for k in ["value"] + includeMeta: ret[parameter.name][k] = snap.get(k, None) return ret -def _singleInstrumentParametersToJson(instrument: InstrumentBase, - get: bool = False, - addPrefix: str = '', - includeMeta: List[str] = [], - excludeParameters: List[str] = [], - simpleFormat: bool = True) -> Dict: +def _singleInstrumentParametersToJson( + instrument: InstrumentBase, + get: bool = False, + addPrefix: str = "", + includeMeta: List[str] = [], + excludeParameters: List[str] = [], + simpleFormat: bool = True, +) -> Dict: """Create a dictionary that holds the parameters of an instrument.""" if "IDN" not in excludeParameters: @@ -254,20 +273,26 @@ def _singleInstrumentParametersToJson(instrument: InstrumentBase, for name, param in instrument.parameters.items(): if (name not in excludeParameters) and (not param.snapshot_exclude): if len(includeMeta) == 0 and simpleFormat: - ret[addPrefix + name] = snap['parameters'][name].get('value', None) + ret[addPrefix + name] = snap["parameters"][name].get("value", None) else: ret[addPrefix + name] = dict() - for k, v in snap['parameters'][name].items(): - if k in (['value'] + includeMeta): + for k, v in snap["parameters"][name].items(): + if k in (["value"] + includeMeta): ret[addPrefix + name][k] = v else: logger.debug(f"excluded: {addPrefix + name}") for name, submod in instrument.submodules.items(): - ret.update(_singleInstrumentParametersToJson( - # FIXME: Fix this mypy ignore - submod, get=get, addPrefix=f"{addPrefix + name}.", # type: ignore[arg-type] - simpleFormat=simpleFormat, includeMeta=includeMeta)) + ret.update( + _singleInstrumentParametersToJson( + # FIXME: Fix this mypy ignore + submod, + get=get, + addPrefix=f"{addPrefix + name}.", # type: ignore[arg-type] + simpleFormat=simpleFormat, + includeMeta=includeMeta, + ) + ) return ret @@ -289,8 +314,7 @@ def _getParamFromList(parent: Any, childrenList: List[str]) -> Parameter: return _getParamFromList(nextObj, childrenList[1:]) -def _getObjectByName(name: str, - src: SerializableType): +def _getObjectByName(name: str, src: SerializableType): """Get an object from a container by specifying its name.""" if isinstance(src, Station): @@ -304,4 +328,3 @@ def _getObjectByName(name: str, if elt.name == name: return elt return None - diff --git a/src/instrumentserver/server/application.py b/src/instrumentserver/server/application.py index 1d88e94..9ae40f9 100644 --- a/src/instrumentserver/server/application.py +++ b/src/instrumentserver/server/application.py @@ -9,10 +9,7 @@ from instrumentserver.client import QtClient from instrumentserver.log import LogLevels, LogWidget, log -from .core import ( - StationServer, - InstrumentModuleBluePrint, ParameterBluePrint -) +from .core import StationServer, InstrumentModuleBluePrint, ParameterBluePrint from .. import QtCore, QtWidgets, QtGui, Client, getInstrumentserverPath from ..gui.misc import DetachableTabWidget, BaseDialog from ..gui.parameters import AnyInputForMethod @@ -35,7 +32,7 @@ class StationList(QtWidgets.QTreeWidget): """A widget that displays all objects in a qcodes station.""" - cols = ['Name', 'Type'] + cols = ["Name", "Type"] #: Signal(str) -- emitted when a parameter or Instrument is selected. #: Argument is the name of the selected instrument @@ -54,7 +51,7 @@ def __init__(self, parent=None): self.clear() self.deleteAction = QtWidgets.QAction("Close Instrument") - self.deleteAction.setShortcuts(['Del', 'backspace']) + self.deleteAction.setShortcuts(["Del", "backspace"]) # you need to add the action to the widget so that it can detect the shortcut self.addAction(self.deleteAction) @@ -62,7 +59,9 @@ def __init__(self, parent=None): self.contextMenu.addAction(self.deleteAction) self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - self.customContextMenuRequested.connect(lambda x: self.contextMenu.exec_(self.mapToGlobal(x))) + self.customContextMenuRequested.connect( + lambda x: self.contextMenu.exec_(self.mapToGlobal(x)) + ) self.deleteAction.triggered.connect(self.onDeleteAction) self.itemSelectionChanged.connect(self._processSelection) @@ -72,7 +71,9 @@ def addInstrument(self, bp: InstrumentModuleBluePrint): self.resizeColumnToContents(0) def removeObject(self, name: str): - items = self.findItems(name, QtCore.Qt.MatchExactly | QtCore.Qt.MatchRecursive, 0) + items = self.findItems( + name, QtCore.Qt.MatchExactly | QtCore.Qt.MatchRecursive, 0 + ) if len(items) > 0: item = items[0] idx = self.indexOfTopLevelItem(item) @@ -94,8 +95,12 @@ def onDeleteAction(self): for item in items: msgBox = QtWidgets.QMessageBox() msgBox.setWindowTitle("Confirm Close Instrument") - msgBox.setText(f'Are you sure you want to close instrument "{item.text(0)}"') - msgBox.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) + msgBox.setText( + f'Are you sure you want to close instrument "{item.text(0)}"' + ) + msgBox.setStandardButtons( + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No + ) msgBox.setDefaultButton(QtWidgets.QMessageBox.No) ret = msgBox.exec() if ret == QtWidgets.QMessageBox.Yes: @@ -103,7 +108,6 @@ def onDeleteAction(self): class StationObjectInfo(QtWidgets.QTextEdit): - def __init__(self, parent=None): super().__init__(parent) @@ -124,19 +128,20 @@ def __init__(self, parent=None): # At the top: a status label, and a button for emitting a test message self.addressLabel = QtWidgets.QLabel() - self.testButton = QtWidgets.QPushButton('Send test message') + self.testButton = QtWidgets.QPushButton("Send test message") self.statusLayout = QtWidgets.QHBoxLayout() self.statusLayout.addWidget(self.addressLabel, 1) self.statusLayout.addWidget(self.testButton, 0) self.testButton.setSizePolicy( - QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, - QtWidgets.QSizePolicy.Minimum) + QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum + ) ) self.layout.addLayout(self.statusLayout) # next row: a window for displaying the incoming messages. - self.layout.addWidget(QtWidgets.QLabel('Messages:')) + self.layout.addWidget(QtWidgets.QLabel("Messages:")) self.messages = QtWidgets.QTextEdit() self.messages.setReadOnly(True) self.layout.addWidget(self.messages) @@ -148,11 +153,11 @@ def setListeningAddress(self, addr: str): @QtCore.Slot(str, str) def addMessageAndReply(self, message: str, reply: str): tstr = time.strftime("%Y-%m-%d %H:%M:%S") - self.messages.setTextColor(QtGui.QColor('black')) + self.messages.setTextColor(QtGui.QColor("black")) self.messages.append(f"[{tstr}]") - self.messages.setTextColor(QtGui.QColor('blue')) + self.messages.setTextColor(QtGui.QColor("blue")) self.messages.append(f"Server received: {message}") - self.messages.setTextColor(QtGui.QColor('green')) + self.messages.setTextColor(QtGui.QColor("green")) self.messages.append(f"Server replied: {reply}") @@ -165,13 +170,20 @@ class CreateInstrumentDialog(BaseDialog): :param insName: Optional, The name of the instrument :param kwargsStr: Optional, String with te args and kwargs separated by commas. """ + createInstrument = QtCore.Signal(str, str, tuple) - def __init__(self, insType: Optional[str] = None, insName: Optional[str] = None, kwargsStr: Optional[str] = None, - parent=None, flags=(QtCore.Qt.CustomizeWindowHint | QtCore.Qt.WindowCloseButtonHint),): + def __init__( + self, + insType: Optional[str] = None, + insName: Optional[str] = None, + kwargsStr: Optional[str] = None, + parent=None, + flags=(QtCore.Qt.CustomizeWindowHint | QtCore.Qt.WindowCloseButtonHint), + ): super().__init__(parent, flags) - tittleText = 'Create New Instrument' + tittleText = "Create New Instrument" self.setWindowTitle(tittleText) layout = QtWidgets.QVBoxLayout(self) @@ -187,15 +199,17 @@ def __init__(self, insType: Optional[str] = None, insName: Optional[str] = None, self.argsEdit.input.setText(kwargsStr) self.argsEdit.doEval.hide() - formLayout.addRow(QtWidgets.QLabel('Instrument Type:'), self.typeEdit) - formLayout.addRow(QtWidgets.QLabel('Instrument Name:'), self.nameEdit) - formLayout.addRow(QtWidgets.QLabel('Args and Kwargs:'), self.argsEdit) + formLayout.addRow(QtWidgets.QLabel("Instrument Type:"), self.typeEdit) + formLayout.addRow(QtWidgets.QLabel("Instrument Name:"), self.nameEdit) + formLayout.addRow(QtWidgets.QLabel("Args and Kwargs:"), self.argsEdit) layout.addLayout(formLayout) self.acceptButton = QtWidgets.QPushButton("Create") self.acceptButton.setDefault(True) - buttonSizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum) + buttonSizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum + ) self.acceptButton.setSizePolicy(buttonSizePolicy) layout.addWidget(self.acceptButton) layout.setAlignment(self.acceptButton, QtCore.Qt.AlignCenter) @@ -242,7 +256,10 @@ class PossibleInstrumentDisplayItem(QtWidgets.QTreeWidgetItem): """ Items used in the PossibleInstrumentDisplay. Need to have a custom one to store extra info. """ - def __init__(self, text, fullInsType, configName=None, lineEdit=None, *args, **kwargs): + + def __init__( + self, text, fullInsType, configName=None, lineEdit=None, *args, **kwargs + ): super().__init__(text, *args, **kwargs) self.configName = configName self.lineEdit = lineEdit @@ -280,8 +297,10 @@ def __init__(self, guiConfig: Optional[dict] = None, *args): self.setColumnCount(len(self.cols)) self.setHeaderLabels(self.cols) - self.basedInstrumentAction = QtWidgets.QAction(f'Create instrument based on this') - self.basedInstrumentAction.setShortcut('N') + self.basedInstrumentAction = QtWidgets.QAction( + f"Create instrument based on this" + ) + self.basedInstrumentAction.setShortcut("N") # you need to add the action to the widget so that it can detect the shortcut self.addAction(self.basedInstrumentAction) @@ -293,10 +312,14 @@ def __init__(self, guiConfig: Optional[dict] = None, *args): self.contextMenu.addAction(self.basedInstrumentAction) self.contextMenu.addSeparator() self.contextMenu.addAction(self.deletePossibleInstrumentAction) - self.customContextMenuRequested.connect(lambda x: self.contextMenu.exec_(self.mapToGlobal(x))) + self.customContextMenuRequested.connect( + lambda x: self.contextMenu.exec_(self.mapToGlobal(x)) + ) self.basedInstrumentAction.triggered.connect(self.onBasedInstrumentAction) - self.deletePossibleInstrumentAction.triggered.connect(self.onRemoveInstrumentFromTree) + self.deletePossibleInstrumentAction.triggered.connect( + self.onRemoveInstrumentFromTree + ) self.config = {} if guiConfig is not None: @@ -308,23 +331,34 @@ def __init__(self, guiConfig: Optional[dict] = None, *args): def loadConfig(self, config: dict): for key, value in config.items(): # In the config, the name of the instrument and the config name are the same. - self.addInstrumentToTree(value['type'], key, key) + self.addInstrumentToTree(value["type"], key, key) self.resizeColumnToContents(0) self.resizeColumnToContents(1) - def addInstrumentToTree(self, fullInsType: str = 'InstrumentType', insName: str = 'MyInstrument', configName: Optional[str]=None): + def addInstrumentToTree( + self, + fullInsType: str = "InstrumentType", + insName: str = "MyInstrument", + configName: Optional[str] = None, + ): """ Each type is grouped together under a parent item of that type. If that parent item does not exist yet it creates it """ - insType = fullInsType.split('.')[-1] - items = self.findItems(insType, QtCore.Qt.MatchExactly | QtCore.Qt.MatchExactly, 0) + insType = fullInsType.split(".")[-1] + items = self.findItems( + insType, QtCore.Qt.MatchExactly | QtCore.Qt.MatchExactly, 0 + ) # Only add the instrument to the tree if there are no other instruments of the same type already if len(items) == 0: parent: PossibleInstrumentDisplayItem | QtWidgets.QTreeWidgetItem = ( - PossibleInstrumentDisplayItem(text=[insType, '', ''], fullInsType=fullInsType,)) + PossibleInstrumentDisplayItem( + text=[insType, "", ""], + fullInsType=fullInsType, + ) + ) self.addTopLevelItem(parent) self.expand(self.indexFromItem(parent, 0)) else: @@ -335,17 +369,23 @@ def addInstrumentToTree(self, fullInsType: str = 'InstrumentType', insName: str createButton = QtWidgets.QPushButton("Create") - lst = [configName, insName, 'create'] + lst = [configName, insName, "create"] lineEdit = QtWidgets.QLineEdit() lineEdit.returnPressed.connect(lambda: createButton.clicked.emit()) lineEdit.setText(insName) - item = PossibleInstrumentDisplayItem(lst, fullInsType=fullInsType, configName=configName, lineEdit=lineEdit) + item = PossibleInstrumentDisplayItem( + lst, fullInsType=fullInsType, configName=configName, lineEdit=lineEdit + ) parent.addChild(item) self.setItemWidget(item, 1, lineEdit) self.setItemWidget(item, 2, createButton) - createButton.clicked.connect(lambda: self.createButtonPressed.emit(configName, fullInsType, lineEdit.text())) + createButton.clicked.connect( + lambda: self.createButtonPressed.emit( + configName, fullInsType, lineEdit.text() + ) + ) def onBasedInstrumentAction(self): items = self.selectedItems() @@ -353,7 +393,9 @@ def onBasedInstrumentAction(self): insName = None if item.lineEdit is not None: insName = item.lineEdit.text() - self.basedInstrumentRequested.emit(item.configName, item.fullInsType, insName) + self.basedInstrumentRequested.emit( + item.configName, item.fullInsType, insName + ) @QtCore.Slot() def onRemoveInstrumentFromTree(self): @@ -389,6 +431,7 @@ class InstrumentsCreator(QtWidgets.QWidget): configs get updated too. :param stationServer: The station server. We just need to connect to some of the signals that it sends """ + #: Signal()-- emitted when the InstrumentCreator creates a new signal. Used to close the creation instrument widget. newInstrumentCreated = QtCore.Signal() @@ -410,32 +453,47 @@ def __init__(self, cli: Client, guiConfig: dict, *args, **kwargs): layout.addWidget(self.possibleInstrumentDisplay) layout.addWidget(self.createNewButton, 0) - self.createNewButton.clicked.connect(lambda: self.onCreateNewInstrumentClicked(None, None, None)) - self.possibleInstrumentDisplay.createButtonPressed.connect(self.onPossibleInstrumentDisplayClicked) - self.possibleInstrumentDisplay.basedInstrumentRequested.connect(self.onCreateNewInstrumentClicked) + self.createNewButton.clicked.connect( + lambda: self.onCreateNewInstrumentClicked(None, None, None) + ) + self.possibleInstrumentDisplay.createButtonPressed.connect( + self.onPossibleInstrumentDisplayClicked + ) + self.possibleInstrumentDisplay.basedInstrumentRequested.connect( + self.onCreateNewInstrumentClicked + ) self.newInstrumentFailed.connect(onExceptionDialog) - self.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum)) + self.setSizePolicy( + QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum + ) + ) @QtCore.Slot(str, str, str) - def onCreateNewInstrumentClicked(self, configName: Optional[str] = None, - insType: Optional[str] = None, - insName: Optional[str] = None): + def onCreateNewInstrumentClicked( + self, + configName: Optional[str] = None, + insType: Optional[str] = None, + insName: Optional[str] = None, + ): # go through all the possible arguments in the config and write them in kwarg form kwargsStr = None if configName is not None and configName in self.guiConfig: conf = self.guiConfig[configName] - kwargsStr = '' - if 'address' in conf: - kwargsStr = kwargsStr + 'address=' + str(conf['address']) - if 'init' in conf: - for k, v in conf['init'].items(): - kwargsStr = kwargsStr + ',' + str(k) + '=' + str(v) + kwargsStr = "" + if "address" in conf: + kwargsStr = kwargsStr + "address=" + str(conf["address"]) + if "init" in conf: + for k, v in conf["init"].items(): + kwargsStr = kwargsStr + "," + str(k) + "=" + str(v) # If there is no address argument the first item will be a comma making the creation crash - if len(kwargsStr) > 0 and kwargsStr[0] == ',': + if len(kwargsStr) > 0 and kwargsStr[0] == ",": kwargsStr = kwargsStr[1:] - dialog = CreateInstrumentDialog(insType=insType, insName=insName, kwargsStr=kwargsStr, parent=self) + dialog = CreateInstrumentDialog( + insType=insType, insName=insName, kwargsStr=kwargsStr, parent=self + ) dialog.createInstrument.connect(self.onDialogNewInstrument) self.newInstrumentCreated.connect(dialog.accept) dialog.exec_() @@ -457,29 +515,42 @@ def onPossibleInstrumentDisplayClicked(self, configName, insType, insName): config. """ if configName in self.guiConfig: - if insName in self.cli.list_instruments(): - self.newInstrumentFailed.emit(f'Instrument with name "{insName}" already exists') + self.newInstrumentFailed.emit( + f'Instrument with name "{insName}" already exists' + ) return # In the qcodes station config, the call the kwargs of the instrument 'init' - kwargs = dict() if 'init' not in self.guiConfig[configName] else dict(self.guiConfig[configName]['init']) - args = [] if 'args' not in self.guiConfig[configName] else self.guiConfig[configName]['args'] - - if 'address' in self.guiConfig[configName]: - kwargs['address'] = self.guiConfig[configName]['address'] + kwargs = ( + dict() + if "init" not in self.guiConfig[configName] + else dict(self.guiConfig[configName]["init"]) + ) + args = ( + [] + if "args" not in self.guiConfig[configName] + else self.guiConfig[configName]["args"] + ) + + if "address" in self.guiConfig[configName]: + kwargs["address"] = self.guiConfig[configName]["address"] self.createNewInstrument(insType, insName, *args, **kwargs) else: - self.newInstrumentFailed.emit("you cannot create instruments that are not in the config from here yet") + self.newInstrumentFailed.emit( + "you cannot create instruments that are not in the config from here yet" + ) def createNewInstrument(self, insType, insName, *args, **kwargs): if insName in self.cli.list_instruments(): self.newInstrumentFailed.emit(f'Instrument "{insName}" already exists.') return try: - self.cli.find_or_create_instrument(name=insName, instrument_class=insType, *args, **kwargs) + self.cli.find_or_create_instrument( + name=insName, instrument_class=insType, *args, **kwargs + ) self.newInstrumentCreated.emit() except Exception as e: self.newInstrumentFailed.emit(str(e)) @@ -490,12 +561,15 @@ class ServerGui(QtWidgets.QMainWindow): serverPortSet = QtCore.Signal(int) - def __init__(self, startServer: Optional[bool] = True, - guiConfig: Optional[dict] = None, - **serverKwargs: Any): + def __init__( + self, + startServer: Optional[bool] = True, + guiConfig: Optional[dict] = None, + **serverKwargs: Any, + ): super().__init__() - self._paramValuesFile = os.path.abspath(os.path.join('.', 'parameters.json')) + self._paramValuesFile = os.path.abspath(os.path.join(".", "parameters.json")) self._bluePrints: dict[str, InstrumentModuleBluePrint] = {} self._serverKwargs = serverKwargs if guiConfig is None: @@ -508,12 +582,19 @@ def __init__(self, startServer: Optional[bool] = True, self.instrumentTabsOpen: dict[str, GenericInstrument] = {} - self.setWindowTitle('Instrument server') + self.setWindowTitle("Instrument server") # Set unique Windows App ID so that this app can have separate taskbar entry than other Qt apps if sys.platform == "win32": import ctypes - ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("InstrumentServer.Server") - self.setWindowIcon(QtGui.QIcon(getInstrumentserverPath("resource", "icons") + "/server_app_icon.svg")) + + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( + "InstrumentServer.Server" + ) + self.setWindowIcon( + QtGui.QIcon( + getInstrumentserverPath("resource", "icons") + "/server_app_icon.svg" + ) + ) # A test client, just a simple helper object. self.client = EmbeddedClient(raise_exceptions=False, timeout=5000) @@ -541,34 +622,37 @@ def __init__(self, startServer: Optional[bool] = True, instrumentsWidgets.addWidget(stationWidgets) instrumentsWidgets.addWidget(self.instrumentCreator) - self.tabs.addUnclosableTab(instrumentsWidgets, 'Station') - self.tabs.addUnclosableTab(LogWidget(level=logging.INFO), 'Log') + self.tabs.addUnclosableTab(instrumentsWidgets, "Station") + self.tabs.addUnclosableTab(LogWidget(level=logging.INFO), "Log") self.serverStatus = ServerStatus() - self.tabs.addUnclosableTab(self.serverStatus, 'Server') + self.tabs.addUnclosableTab(self.serverStatus, "Server") # Toolbar. - self.toolBar = self.addToolBar('Tools') + self.toolBar = self.addToolBar("Tools") self.toolBar.setIconSize(QtCore.QSize(16, 16)) # Station tools. - self.toolBar.addWidget(QtWidgets.QLabel('Station:')) + self.toolBar.addWidget(QtWidgets.QLabel("Station:")) self.refreshStationAction = QtWidgets.QAction( - QtGui.QIcon(":/icons/refresh.svg"), 'Refresh', self) + QtGui.QIcon(":/icons/refresh.svg"), "Refresh", self + ) self.refreshStationAction.triggered.connect(self.refreshStationComponents) self.toolBar.addAction(self.refreshStationAction) # Parameter tools. self.toolBar.addSeparator() - self.toolBar.addWidget(QtWidgets.QLabel('Params:')) + self.toolBar.addWidget(QtWidgets.QLabel("Params:")) self.loadParamsAction = QtWidgets.QAction( - QtGui.QIcon(":/icons/load.svg"), 'Load from file', self) + QtGui.QIcon(":/icons/load.svg"), "Load from file", self + ) self.loadParamsAction.triggered.connect(self.loadParamsFromFile) self.toolBar.addAction(self.loadParamsAction) self.saveParamsAction = QtWidgets.QAction( - QtGui.QIcon(":/icons/save.svg"), 'Save to file', self) + QtGui.QIcon(":/icons/save.svg"), "Save to file", self + ) self.saveParamsAction.triggered.connect(self.saveParamsToFile) self.toolBar.addAction(self.saveParamsAction) @@ -597,7 +681,10 @@ def closeEvent(self, event): pass self.instrumentTabsOpen.clear() - if hasattr(self, 'stationServerThread') and self.stationServerThread is not None: + if ( + hasattr(self, "stationServerThread") + and self.stationServerThread is not None + ): if self.stationServerThread.isRunning(): try: self.client.ask(self.stationServer.SAFEWORD) @@ -616,7 +703,7 @@ def startServer(self): self.stationServerThread = QtCore.QThread() self.stationServer.moveToThread(self.stationServerThread) self.stationServerThread.started.connect(self.stationServer.startServer) - self.stationServer.finished.connect(lambda: self.log('ZMQ server closed.')) + self.stationServer.finished.connect(lambda: self.log("ZMQ server closed.")) self.stationServer.finished.connect(self.stationServerThread.quit) self.stationServer.finished.connect(self.stationServer.deleteLater) @@ -625,7 +712,7 @@ def startServer(self): self.stationServer.serverStarted.connect(self.client.start) self.stationServer.serverStarted.connect(self.refreshStationComponents) self.stationServer.finished.connect( - lambda: self.log('Server thread finished.', LogLevels.info) + lambda: self.log("Server thread finished.", LogLevels.info) ) self.stationServer.messageReceived.connect(self._messageReceived) self.stationServer.instrumentCreated.connect(self.addInstrumentToGui) @@ -652,7 +739,9 @@ def _messageReceived(self, message: str, reply: str): self.log(f"Server replied: {reply}", LogLevels.debug) self.serverStatus.addMessageAndReply(messageSummary, replySummary) - def addInstrumentToGui(self, instrumentBluePrint: InstrumentModuleBluePrint, insArgs, insKwargs): + def addInstrumentToGui( + self, instrumentBluePrint: InstrumentModuleBluePrint, insArgs, insKwargs + ): """ Add an instrument to the station list. @@ -664,17 +753,22 @@ def addInstrumentToGui(self, instrumentBluePrint: InstrumentModuleBluePrint, ins if instrumentBluePrint.name not in self._guiConfig: # add the gui config for opening generic GUI's and keep track of the config if insArgs is None or insArgs == []: - self._guiConfig[instrumentBluePrint.name] = dict(gui=GUIFIELD, - type=instrumentBluePrint.instrument_module_class, - init=insKwargs) + self._guiConfig[instrumentBluePrint.name] = dict( + gui=GUIFIELD, + type=instrumentBluePrint.instrument_module_class, + init=insKwargs, + ) else: - self._guiConfig[instrumentBluePrint.name] = dict(gui=GUIFIELD, - type=instrumentBluePrint.instrument_module_class, - args=insArgs, - init=insKwargs) + self._guiConfig[instrumentBluePrint.name] = dict( + gui=GUIFIELD, + type=instrumentBluePrint.instrument_module_class, + args=insArgs, + init=insKwargs, + ) self.instrumentCreator.possibleInstrumentDisplay.addInstrumentToTree( - instrumentBluePrint.instrument_module_class, instrumentBluePrint.name) + instrumentBluePrint.instrument_module_class, instrumentBluePrint.name + ) def removeInstrumentFromGui(self, name: str): """Remove an instrument from the station list.""" @@ -687,7 +781,7 @@ def removeInstrumentFromGui(self, name: str): def refreshStationComponents(self): """Clear and re-populate the widget holding the station components, using the objects that are currently registered in the station.""" - if getattr(self.client, '_closed', False) or not self.client.connected: + if getattr(self.client, "_closed", False) or not self.client.connected: return self.stationList.clear() try: @@ -706,8 +800,9 @@ def loadParamsFromFile(self): """Load the values of all parameters present in the server's params json file to parameters registered in the station (incl those in instruments).""" - logger.info(f"Loading parameters from file: " - f"{os.path.abspath(self._paramValuesFile)}") + logger.info( + f"Loading parameters from file: {os.path.abspath(self._paramValuesFile)}" + ) try: self.client.paramsFromFile(self._paramValuesFile) except Exception as e: @@ -715,10 +810,11 @@ def loadParamsFromFile(self): def saveParamsToFile(self): """Save the values of all parameters registered in the station (incl - those in instruments) to the server's param json file.""" + those in instruments) to the server's param json file.""" - logger.info(f"Saving parameters to file: " - f"{os.path.abspath(self._paramValuesFile)}") + logger.info( + f"Saving parameters to file: {os.path.abspath(self._paramValuesFile)}" + ) try: self.client.paramsToFile(self._paramValuesFile) except Exception as e: @@ -747,14 +843,16 @@ def addInstrumentTab(self, item: QtWidgets.QTreeWidgetItem, index: int): # The user might create an instrument that is not in the config file if name in self._guiConfig: # import the widget - moduleName = '.'.join(self._guiConfig[name]['gui']['type'].split('.')[:-1]) - widgetClassName = self._guiConfig[name]['gui']['type'].split('.')[-1] + moduleName = ".".join( + self._guiConfig[name]["gui"]["type"].split(".")[:-1] + ) + widgetClassName = self._guiConfig[name]["gui"]["type"].split(".")[-1] module = importlib.import_module(moduleName) widgetClass = getattr(module, widgetClassName) # get any kwargs if the config file has any - if 'kwargs' in self._guiConfig[name]['gui']: - kwargs = self._guiConfig[name]['gui']['kwargs'] + if "kwargs" in self._guiConfig[name]["gui"]: + kwargs = self._guiConfig[name]["gui"]["kwargs"] kwargs["sub_port"] = kwargs.get("sub_port", self.stationServer.port + 1) insWidget = widgetClass(ins, parent=self, **kwargs) @@ -772,7 +870,7 @@ def onTabDeleted(self, name: str) -> None: @QtCore.Slot(str, object, object, object) def onFuncCalled(self, n, args, kw, ret): - if n == 'close_and_remove_instrument': + if n == "close_and_remove_instrument": for ins in args: self.removeInstrumentFromGui(ins) @@ -785,7 +883,7 @@ def closeInstrument(self, ins): class DetachedServerGui(QtWidgets.QMainWindow): """A detached version of the server gui.""" - def __init__(self, host: str = 'localhost', port: int = 5555): + def __init__(self, host: str = "localhost", port: int = 5555): super().__init__() self.instrumentTabsOpen: dict[str, GenericInstrument] = {} @@ -793,7 +891,7 @@ def __init__(self, host: str = 'localhost', port: int = 5555): self.client = Client(host, port, timeout=20) self.subClient = None - self.setWindowTitle('Instrument server detached') + self.setWindowTitle("Instrument server detached") self.tabs = DetachableTabWidget(self) self.tabs.onTabClosed.connect(self.onTabDeleted) @@ -810,16 +908,17 @@ def __init__(self, host: str = 'localhost', port: int = 5555): stationWidgets.addWidget(self.stationObjInfo) stationWidgets.setSizes([300, 500]) - self.tabs.addUnclosableTab(stationWidgets, 'Station') + self.tabs.addUnclosableTab(stationWidgets, "Station") # Toolbar. - self.toolBar = self.addToolBar('Tools') + self.toolBar = self.addToolBar("Tools") self.toolBar.setIconSize(QtCore.QSize(16, 16)) # Station tools. - self.toolBar.addWidget(QtWidgets.QLabel('Station:')) + self.toolBar.addWidget(QtWidgets.QLabel("Station:")) self.refreshStationAction = QtWidgets.QAction( - QtGui.QIcon(":/icons/refresh.svg"), 'Refresh', self) + QtGui.QIcon(":/icons/refresh.svg"), "Refresh", self + ) self.refreshStationAction.triggered.connect(self.refreshStationComponents) self.toolBar.addAction(self.refreshStationAction) @@ -848,13 +947,13 @@ def addInstrumentTab(self, item: QtWidgets.QTreeWidgetItem, index: int): kwargs = {} try: guiConfig = self.client._getGuiConfig(name) - moduleName = '.'.join(guiConfig['gui']['type'].split('.')[:-1]) - widgetClassName = guiConfig['gui']['type'].split('.')[-1] + moduleName = ".".join(guiConfig["gui"]["type"].split(".")[:-1]) + widgetClassName = guiConfig["gui"]["type"].split(".")[-1] module = importlib.import_module(moduleName) widgetClass = getattr(module, widgetClassName) - if 'kwargs' in guiConfig['gui']: - kwargs = guiConfig[name]['gui']['kwargs'] + if "kwargs" in guiConfig["gui"]: + kwargs = guiConfig[name]["gui"]["kwargs"] # If the instrument does not have a guiconfig an exception is raised. just use defaults values except Exception as e: @@ -874,10 +973,10 @@ def onTabDeleted(self, name: str) -> None: del self.instrumentTabsOpen[name] -def startServerGuiApplication(guiConfig: Optional[Dict[str, Dict[str, Any]]] = None, - **serverKwargs: Any) -> "ServerGui": - """Create a server gui window. - """ +def startServerGuiApplication( + guiConfig: Optional[Dict[str, Dict[str, Any]]] = None, **serverKwargs: Any +) -> "ServerGui": + """Create a server gui window.""" window = ServerGui(startServer=True, guiConfig=guiConfig, **serverKwargs) window.show() return window @@ -891,7 +990,7 @@ class EmbeddedClient(QtClient): def start(self, addr: str): if self._closed: return - self.addr = "tcp://localhost:" + addr.split(':')[-1] + self.addr = "tcp://localhost:" + addr.split(":")[-1] self.connect() @QtCore.Slot(str) @@ -922,11 +1021,11 @@ def bluePrintToHtml(bp: Union[ParameterBluePrint, InstrumentModuleBluePrint]): def parameterToHtml(bp: ParameterBluePrint, headerLevel=None): setget = [] - setgetstr = '' + setgetstr = "" if bp.gettable: - setget.append('get') + setget.append("get") if bp.settable: - setget.append('set') + setget.append("set") if len(setget) > 0: setgetstr = f"[{', '.join(setget)}]" @@ -1021,4 +1120,4 @@ def instrumentToHtml(bp: InstrumentModuleBluePrint): div.instrument_container { padding: 10px; } -""" \ No newline at end of file +""" diff --git a/src/instrumentserver/server/core.py b/src/instrumentserver/server/core.py index 5821bd1..57c5344 100644 --- a/src/instrumentserver/server/core.py +++ b/src/instrumentserver/server/core.py @@ -34,21 +34,39 @@ import qcodes as qc from qcodes import ( - Station, Instrument, InstrumentChannel, Parameter, ParameterWithSetpoints) + Station, + Instrument, + InstrumentChannel, + Parameter, + ParameterWithSetpoints, +) from qcodes.instrument.base import InstrumentBase from qcodes.utils.validators import Validator from .. import QtCore, serialize -from ..blueprints import (ParameterBluePrint, MethodBluePrint, InstrumentModuleBluePrint, ParameterBroadcastBluePrint, - bluePrintFromMethod, bluePrintFromInstrumentModule, bluePrintFromParameter, - INSTRUMENT_MODULE_BASE_CLASSES, PARAMETER_BASE_CLASSES, Operation, - InstrumentCreationSpec, CallSpec, ParameterSerializeSpec, ServerInstruction, ServerResponse,) +from ..blueprints import ( + ParameterBluePrint, + MethodBluePrint, + InstrumentModuleBluePrint, + ParameterBroadcastBluePrint, + bluePrintFromMethod, + bluePrintFromInstrumentModule, + bluePrintFromParameter, + INSTRUMENT_MODULE_BASE_CLASSES, + PARAMETER_BASE_CLASSES, + Operation, + InstrumentCreationSpec, + CallSpec, + ParameterSerializeSpec, + ServerInstruction, + ServerResponse, +) from ..base import send_router, recv_router, sendBroadcast from ..helpers import nestedAttributeFromString, objectClassPath, typeClassPath -__author__ = 'Wolfgang Pfaff', 'Chao Zhou' -__license__ = 'MIT' +__author__ = "Wolfgang Pfaff", "Chao Zhou" +__license__ = "MIT" logger = logging.getLogger(__name__) @@ -66,7 +84,7 @@ class StationServer(QtCore.QObject): # If this string is sent as message to the server, it'll shut down and close # the socket. Should only be used from within this module. # It's randomized in the instantiated server for a little bit of safety. - SAFEWORD = 'BANANA' + SAFEWORD = "BANANA" #: Signal(str, str) -- emit messages for display in the gui (or other stuff the gui #: wants to do with it. @@ -96,31 +114,34 @@ class StationServer(QtCore.QObject): #: Arguments: full function location as string, arguments, kw arguments, return value. funcCalled = QtCore.Signal(str, object, object, object) - def __init__(self, - parent: Optional[QtCore.QObject] = None, - port: int = 5555, - allowUserShutdown: bool = False, - addresses: List[str] = [], - initScript: Optional[str] = None, - serverConfig: Optional[Dict[str, Any]] = None, - stationConfig: Optional[str] = None, - guiConfig: Optional[dict[str, Any]] = None, - pollingThread: Optional[QtCore.QThread] = None, - ipAddresses: Optional[Dict[str, str]] = None - ) -> None: + def __init__( + self, + parent: Optional[QtCore.QObject] = None, + port: int = 5555, + allowUserShutdown: bool = False, + addresses: List[str] = [], + initScript: Optional[str] = None, + serverConfig: Optional[Dict[str, Any]] = None, + stationConfig: Optional[str] = None, + guiConfig: Optional[dict[str, Any]] = None, + pollingThread: Optional[QtCore.QThread] = None, + ipAddresses: Optional[Dict[str, str]] = None, + ) -> None: super().__init__(parent) if addresses is None: addresses = [] if initScript is None: - initScript = '' + initScript = "" - if (ipAddresses is not None - and 'listeningAddress' in ipAddresses - and (listening_addr := ipAddresses.get('listeningAddress')) is not None): + if ( + ipAddresses is not None + and "listeningAddress" in ipAddresses + and (listening_addr := ipAddresses.get("listeningAddress")) is not None + ): addresses.append(listening_addr) - self.SAFEWORD = ''.join(random.choices([chr(i) for i in range(65, 91)], k=16)) + self.SAFEWORD = "".join(random.choices([chr(i) for i in range(65, 91)], k=16)) self.serverRunning = False self.port = int(port) self.serverConfig = serverConfig @@ -132,19 +153,23 @@ def __init__(self, # For now the only server configs are whether to start an instrument. if self.serverConfig is not None: for instrumentName, settings in self.serverConfig.items(): - if settings['initialize']: + if settings["initialize"]: self.station.load_instrument(instrumentName) self.allowUserShutdown = allowUserShutdown - self.listenAddresses = list(set(['127.0.0.1'] + addresses)) + self.listenAddresses = list(set(["127.0.0.1"] + addresses)) self.initScript = initScript self.broadcastPort = self.port + 1 self.broadcastSocket: zmq.Socket | None = None self.externalBroadcastAddr = None - if ipAddresses is not None and 'externalBroadcast' in ipAddresses and ipAddresses.get('externalBroadcast') is not None: - self.externalBroadcastAddr = ipAddresses.get('externalBroadcast') + if ( + ipAddresses is not None + and "externalBroadcast" in ipAddresses + and ipAddresses.get("externalBroadcast") is not None + ): + self.externalBroadcastAddr = ipAddresses.get("externalBroadcast") self.externalBroadcastSocket: zmq.Socket | None = None self.pollingThread = pollingThread @@ -158,18 +183,18 @@ def __init__(self, lambda n, v: logger.info(f"Parameter '{n}' retrieved: {str(v)}") ) self.funcCalled.connect( - lambda n, args, kw, ret: logger.info(f"Function called:" - f"'{n}', args: {str(args)}, " - f"kwargs: {str(kw)})'.") + lambda n, args, kw, ret: logger.info( + f"Function called:'{n}', args: {str(args)}, kwargs: {str(kw)})'." + ) ) - + # a queue for responses that are ready to be sent to client self._response_queue = queue.Queue() # a socket pair for immediate wakeup of the main thread that sends response to client self._wakeup_r, self._wakeup_w = socket.socketpair() self._wakeup_r.setblocking(False) self._wakeup_w.setblocking(False) - + # Per-instrument locks to avoid races when multiple threads talk to the same instrument concurrently self._instrument_locks: dict[str, threading.RLock] = {} self._instrument_locks_lock = threading.Lock() @@ -216,22 +241,26 @@ def startServer(self) -> bool: logger.info(f"Not broadcasting to external address") self.serverRunning = True - if self.initScript not in ['', None]: + if self.initScript not in ["", None]: logger.info(f"Running init script") self._runInitScript() - + # create a thread pool for handling incoming client requests concurrently with ThreadPoolExecutor() as pool: while self.serverRunning or not self._response_queue.empty(): try: # check if there is either incoming request from client, or a processing worker has finished socks = dict(poller.poll(10)) - + # handle router socket events (incoming requests) - if self.serverRunning and socket in socks and (socks[socket] & zmq.POLLIN): + if ( + self.serverRunning + and socket in socks + and (socks[socket] & zmq.POLLIN) + ): identity, message = recv_router(socket) pool.submit(self._handleRouterMessage, identity, message) - + # handle wakeup events (one or more workers finished) if self._wakeup_r in socks and (socks[self._wakeup_r] & zmq.POLLIN): # Drain the wakeup pipe so it doesn't stay "always readable" @@ -240,30 +269,34 @@ def startServer(self) -> bool: self._wakeup_r.recv(1024) except BlockingIOError: pass - + # drain completed responses from workers while True: try: - identity, response_to_client, response_log, shutdown = self._response_queue.get_nowait() + identity, response_to_client, response_log, shutdown = ( + self._response_queue.get_nowait() + ) except queue.Empty: break - + try: send_router(socket, identity, response_to_client) except Exception as e: logger.error(f"Failed to send response to client: {e}") - + # emit log signal - self.messageReceived.emit(str(response_to_client.message), response_log) - + self.messageReceived.emit( + str(response_to_client.message), response_log + ) + # flip the shutdown flag in the main thread if shutdown: self.serverRunning = False - + except Exception as e: logger.exception(f"Unexpected error in server loop: {e}") break - + socket.close(linger=0) self._wakeup_r.close() self._wakeup_w.close() @@ -280,27 +313,27 @@ def startServer(self) -> bool: self.finished.emit() logger.info("StationServer shut down cleanly.") return True - + def _handleRouterMessage(self, identity, message): """ Handle a router message and put the response message in the response queue. - + """ message_ok = True response_to_client = None response_log = None - shutdown = False # flag for letting the main thread shut down the server + shutdown = False # flag for letting the main thread shut down the server # Allow the test client from within the same process to make sure the # server shuts down. if message == self.SAFEWORD: - response_log = 'Server has received the safeword and will shut down.' + response_log = "Server has received the safeword and will shut down." response_to_client = ServerResponse(message=response_log) shutdown = True logger.warning(response_log) - elif self.allowUserShutdown and message == 'SHUTDOWN': - response_log = 'Server shutdown requested by client.' + elif self.allowUserShutdown and message == "SHUTDOWN": + response_log = "Server shutdown requested by client." response_to_client = ServerResponse(message=response_log) shutdown = True logger.warning(response_log) @@ -317,13 +350,13 @@ def _handleRouterMessage(self, identity, message): instruction = message try: instruction.validate() - logger.debug(f"Received request for operation: " - f"{str(instruction.operation)}") - logger.debug(f"Instruction received: " - f"{str(instruction)}") + logger.debug( + f"Received request for operation: {str(instruction.operation)}" + ) + logger.debug(f"Instruction received: {str(instruction)}") except Exception as e: message_ok = False - response_log = f'Received invalid message. Error raised: {str(e)}' + response_log = f"Received invalid message. Error raised: {str(e)}" response_to_client = ServerResponse(message=None, error=e) logger.warning(response_log) @@ -343,7 +376,7 @@ def _handleRouterMessage(self, identity, message): response_to_client = ServerResponse(message=None, error=response_log) logger.warning(f"Invalid message type: {type(message)}.") logger.debug(f"Invalid message received: {str(message)}") - + self._response_queue.put((identity, response_to_client, response_log, shutdown)) # wake up the server loop so it can send the response immediately try: @@ -351,9 +384,10 @@ def _handleRouterMessage(self, identity, message): except OSError: # If we're shutting down / socket closed, ignore pass - - def executeServerInstruction(self, instruction: ServerInstruction) \ - -> Tuple[ServerResponse, str]: + + def executeServerInstruction( + self, instruction: ServerInstruction + ) -> Tuple[ServerResponse, str]: """ This is the interpreter function that the server will call to translate the dictionary received from the proxy to instrument calls. @@ -412,59 +446,71 @@ def _getExistingInstruments(self) -> List[str]: def _createInstrument(self, spec: InstrumentCreationSpec) -> None: """Create a new instrument on the server.""" - sep_class = spec.instrument_class.split('.') - modName = '.'.join(sep_class[:-1]) + sep_class = spec.instrument_class.split(".") + modName = ".".join(sep_class[:-1]) clsName = sep_class[-1] mod = importlib.import_module(modName) cls = getattr(mod, clsName) args = [] if spec.args is None else spec.args kwargs = dict() if spec.kwargs is None else spec.kwargs - + # lock based on the intended instrument name lock = self._get_lock_for_target(spec.name) if lock is None: # in case name isn't in station yet, just guard creation with the dict lock - lock = self._instrument_locks_lock # coarse but fine for this rare operation + lock = ( + self._instrument_locks_lock + ) # coarse but fine for this rare operation with lock: new_instrument = qc.find_or_create_instrument( - cls, spec.name, *args, **kwargs) - + cls, spec.name, *args, **kwargs + ) + if new_instrument.name not in self.station.components: self.station.add_component(new_instrument) - - self.instrumentCreated.emit(bluePrintFromInstrumentModule(new_instrument.name, new_instrument), - args, kwargs) + + self.instrumentCreated.emit( + bluePrintFromInstrumentModule(new_instrument.name, new_instrument), + args, + kwargs, + ) def _callObject(self, spec: CallSpec) -> Any: """Call some callable found in the station.""" obj = nestedAttributeFromString(self.station, spec.target) args = spec.args if spec.args is not None else [] kwargs = spec.kwargs if spec.kwargs is not None else {} - + def _invoke(): ret = obj(*args, **kwargs) - + # Check if a new parameter is being created. self._newOrDeleteParameterDetection(spec, args, kwargs) - + if isinstance(obj, Parameter): if len(args) > 0: self.parameterSet.emit(spec.target, args[0]) - + # Broadcast changes in parameter values. - self._broadcastParameterChange(ParameterBroadcastBluePrint(spec.target, 'parameter-update', args[0])) + self._broadcastParameterChange( + ParameterBroadcastBluePrint( + spec.target, "parameter-update", args[0] + ) + ) else: self.parameterGet.emit(spec.target, ret) - + # Broadcast calls of parameters. - self._broadcastParameterChange(ParameterBroadcastBluePrint(spec.target, 'parameter-call', ret)) + self._broadcastParameterChange( + ParameterBroadcastBluePrint(spec.target, "parameter-call", ret) + ) else: self.funcCalled.emit(spec.target, args, kwargs, ret) - + return ret - + # Get the appropriate per-instrument lock, if any lock = self._get_lock_for_target(spec.target) if lock is None: @@ -475,28 +521,30 @@ def _invoke(): with lock: return _invoke() - def _getBluePrint(self, path: str) -> Union[InstrumentModuleBluePrint, - ParameterBluePrint, - MethodBluePrint]: + def _getBluePrint( + self, path: str + ) -> Union[InstrumentModuleBluePrint, ParameterBluePrint, MethodBluePrint]: logger.debug(f"Fetching blueprint for: {path}") obj = nestedAttributeFromString(self.station, path) if isinstance(obj, tuple(INSTRUMENT_MODULE_BASE_CLASSES)): instrument_blueprint = bluePrintFromInstrumentModule(path, obj) if instrument_blueprint is None: - raise ValueError(f'Failed to create blueprint for instrument module {path}') + raise ValueError( + f"Failed to create blueprint for instrument module {path}" + ) return instrument_blueprint elif isinstance(obj, tuple(PARAMETER_BASE_CLASSES)): parameter_blueprint = bluePrintFromParameter(path, obj) if parameter_blueprint is None: - raise ValueError(f'Failed to create blueprint for parameter {path}') + raise ValueError(f"Failed to create blueprint for parameter {path}") return parameter_blueprint elif callable(obj): method_blueprint = bluePrintFromMethod(path, obj) if method_blueprint is None: - raise ValueError(f'Failed to create blueprint for method {path}') + raise ValueError(f"Failed to create blueprint for method {path}") return method_blueprint else: - raise ValueError(f'Cannot create a blueprint for {type(obj)}') + raise ValueError(f"Cannot create a blueprint for {type(obj)}") def _toParamDict(self, opts: ParameterSerializeSpec) -> Dict[str, Any]: obj: list[Any] | Station @@ -505,7 +553,7 @@ def _toParamDict(self, opts: ParameterSerializeSpec) -> Dict[str, Any]: else: obj = [nestedAttributeFromString(self.station, opts.path)] - includeMeta = [k for k in opts.attrs if k != 'value'] + includeMeta = [k for k in opts.attrs if k != "value"] args = opts.args if opts.args else [] kwargs = dict(opts.kwargs) if opts.kwargs else {} kwargs.update(includeMeta=includeMeta) @@ -520,7 +568,7 @@ def _getGuiConfig(self, instrumentName: str) -> str: """ if self.station is None: raise ValueError("Station is not initialized.") - + if instrumentName not in self.station.components: raise ValueError(f"Instrument {instrumentName} not found in station.") @@ -539,11 +587,15 @@ def _broadcastParameterChange(self, blueprint: ParameterBroadcastBluePrint): :param blueprint: The parameter broadcast blueprint that is being broadcast """ - sendBroadcast(self.broadcastSocket, blueprint.name.split('.')[0], blueprint) + sendBroadcast(self.broadcastSocket, blueprint.name.split(".")[0], blueprint) if self.externalBroadcastAddr is not None: - sendBroadcast(self.externalBroadcastSocket, blueprint.name.split('.')[0], blueprint) - logger.info(f"Parameter {blueprint.name} has broadcast an update of type: {blueprint.action}," - f" with a value: {blueprint.value}.") + sendBroadcast( + self.externalBroadcastSocket, blueprint.name.split(".")[0], blueprint + ) + logger.info( + f"Parameter {blueprint.name} has broadcast an update of type: {blueprint.action}," + f" with a value: {blueprint.value}." + ) def _newOrDeleteParameterDetection(self, spec, args, kwargs): """ @@ -555,19 +607,17 @@ def _newOrDeleteParameterDetection(self, spec, args, kwargs): :param kwargs: kwargs being passed to the call method. """ - if spec.target.split('.')[-1] == 'add_parameter': - name = spec.target.split('.')[0] + '.' + '.'.join(spec.args) - pb = ParameterBroadcastBluePrint(name, - 'parameter-creation', - kwargs['initial_value'], - kwargs['unit']) + if spec.target.split(".")[-1] == "add_parameter": + name = spec.target.split(".")[0] + "." + ".".join(spec.args) + pb = ParameterBroadcastBluePrint( + name, "parameter-creation", kwargs["initial_value"], kwargs["unit"] + ) self._broadcastParameterChange(pb) - elif spec.target.split('.')[-1] == 'remove_parameter': - name = spec.target.split('.')[0] + '.' + '.'.join(spec.args) - pb = ParameterBroadcastBluePrint(name, - 'parameter-deletion') + elif spec.target.split(".")[-1] == "remove_parameter": + name = spec.target.split(".")[0] + "." + ".".join(spec.args) + pb = ParameterBroadcastBluePrint(name, "parameter-deletion") self._broadcastParameterChange(pb) - + def _get_lock_for_target(self, target: str) -> Optional[threading.RLock]: """ Given a call target like 'dac1.ch1.offset' or 'awg.ch2.set_sq_wave', @@ -581,8 +631,8 @@ def _get_lock_for_target(self, target: str) -> Optional[threading.RLock]: return None # First token before the first dot: assumed to be instrument name - root = target.split('.')[0] - + root = target.split(".")[0] + # Only lock if this actually corresponds to an instrument in the station if root not in self.station.components: return None @@ -594,29 +644,33 @@ def _get_lock_for_target(self, target: str) -> Optional[threading.RLock]: self._instrument_locks[root] = lock return lock -def startServer(port: int = 5555, - allowUserShutdown: bool = False, - addresses: List[str] = [], - initScript: Optional[str] = None, - serverConfig: Optional[Dict[str, Any]] = None, - stationConfig: Optional[str] = None, - guiConfig: Optional[dict[str, Any]] = None, - pollingThread: Optional[QtCore.QThread] = None, - ipAddresses: Optional[Dict[str, str]] = None) -> \ - Tuple[StationServer, QtCore.QThread]: + +def startServer( + port: int = 5555, + allowUserShutdown: bool = False, + addresses: List[str] = [], + initScript: Optional[str] = None, + serverConfig: Optional[Dict[str, Any]] = None, + stationConfig: Optional[str] = None, + guiConfig: Optional[dict[str, Any]] = None, + pollingThread: Optional[QtCore.QThread] = None, + ipAddresses: Optional[Dict[str, str]] = None, +) -> Tuple[StationServer, QtCore.QThread]: """Create a server and run in a separate thread. :returns: The server object and the thread it's running in. """ - server = StationServer(port=port, - allowUserShutdown=allowUserShutdown, - addresses=addresses, - initScript=initScript, - serverConfig=serverConfig, - stationConfig=stationConfig, - guiConfig=guiConfig, - pollingThread=pollingThread, - ipAddresses=ipAddresses) + server = StationServer( + port=port, + allowUserShutdown=allowUserShutdown, + addresses=addresses, + initScript=initScript, + serverConfig=serverConfig, + stationConfig=stationConfig, + guiConfig=guiConfig, + pollingThread=pollingThread, + ipAddresses=ipAddresses, + ) thread = QtCore.QThread() server.moveToThread(thread) server.finished.connect(thread.quit) diff --git a/src/instrumentserver/server/pollingWorker.py b/src/instrumentserver/server/pollingWorker.py index b790db7..dbc16d0 100644 --- a/src/instrumentserver/server/pollingWorker.py +++ b/src/instrumentserver/server/pollingWorker.py @@ -10,10 +10,12 @@ class PollingWorker(QtCore.QThread): - def __init__(self, pollingRates: Optional[Dict[str, int]]=None): + def __init__(self, pollingRates: Optional[Dict[str, int]] = None): super().__init__(None) # This worker is supposed to only run through the server itself so there is no need to change the defaults of the client. - self.cli = Client(raise_exceptions=False,timeout=60000) # Don't raise exceptions on timeouts + self.cli = Client( + raise_exceptions=False, timeout=60000 + ) # Don't raise exceptions on timeouts self.pollingRates = pollingRates # Used by the qtimers, get value of the param @@ -42,9 +44,11 @@ def run(self): delList.append(param) for item in delList: del self.pollingRates[item] - + # Prints which parameters are being polled - logger.info(f"Broadcasting the following parameters: {list(self.pollingRates.keys())}") + logger.info( + f"Broadcasting the following parameters: {list(self.pollingRates.keys())}" + ) # Creates timers for each param in the dict for param in self.pollingRates: diff --git a/src/instrumentserver/testing/create_instrument.py b/src/instrumentserver/testing/create_instrument.py index 9949f7b..bb7b366 100644 --- a/src/instrumentserver/testing/create_instrument.py +++ b/src/instrumentserver/testing/create_instrument.py @@ -1,4 +1,5 @@ from instrumentserver.client import Client as InstrumentClient + """ Script used to create an instrument in the instrument server used for developing the dashboard/logger. """ @@ -7,10 +8,10 @@ # used for testing, the instruments should be already created for the dashboard to work cli = InstrumentClient() -if 'test' in cli.list_instruments(): - instrument = cli.get_instrument('test') +if "test" in cli.list_instruments(): + instrument = cli.get_instrument("test") else: instrument = cli.find_or_create_instrument( - 'test' - 'instrumentserver.testing.dummy_instruments.generic.DummyInstrumentRandomNumber',) - + "test" + "instrumentserver.testing.dummy_instruments.generic.DummyInstrumentRandomNumber", + ) diff --git a/src/instrumentserver/testing/dummy_instruments/generic.py b/src/instrumentserver/testing/dummy_instruments/generic.py index bde62de..128a8cf 100644 --- a/src/instrumentserver/testing/dummy_instruments/generic.py +++ b/src/instrumentserver/testing/dummy_instruments/generic.py @@ -13,55 +13,64 @@ class DummyChannel(Instrument): def __init__(self, name: str, *args, **kwargs): super().__init__(name, *args, **kwargs) - self.add_parameter('ch0', - set_cmd=None, - vals=validators.Numbers(0, 1), - initial_value=0) - - self.add_parameter('ch1', unit='v', - set_cmd=None, - vals=validators.Numbers(-1, 1), - initial_value=1) + self.add_parameter( + "ch0", set_cmd=None, vals=validators.Numbers(0, 1), initial_value=0 + ) + + self.add_parameter( + "ch1", + unit="v", + set_cmd=None, + vals=validators.Numbers(-1, 1), + initial_value=1, + ) def dummy_function(self, *args, **kwargs): """Dummy function for specific channels used for testing""" - print(f'the dummy chanel: {self.name} has been activated with:') - print(f'args: {args}') - print(f'kwargs: {kwargs}') + print(f"the dummy chanel: {self.name} has been activated with:") + print(f"args: {args}") + print(f"kwargs: {kwargs}") return True class DummyInstrumentWithSubmodule(Instrument): """A dummy instrument with submodules.""" - def __init__(self, name: str, address=None, first_arg=None, second_arg=None, *args, **kwargs): + def __init__( + self, name: str, address=None, first_arg=None, second_arg=None, *args, **kwargs + ): super().__init__(name, *args, **kwargs) self.address = address self.first_arg = first_arg self.second_arg = second_arg - self.add_parameter('param0', - set_cmd=None, - vals=validators.Numbers(0, 1), - initial_value=0) - - self.add_parameter('param1', unit='v', - set_cmd=None, - vals=validators.Numbers(-1, 1), - initial_value=1) - - self.add_parameter('int_param1', unit='v', - set_cmd=None, - vals=validators.Ints(-200, 200), - initial_value=1) - - for chan_name in ('A', 'B', 'C'): - channel = DummyChannel(f'{name}_Chan{chan_name}') + self.add_parameter( + "param0", set_cmd=None, vals=validators.Numbers(0, 1), initial_value=0 + ) + + self.add_parameter( + "param1", + unit="v", + set_cmd=None, + vals=validators.Numbers(-1, 1), + initial_value=1, + ) + + self.add_parameter( + "int_param1", + unit="v", + set_cmd=None, + vals=validators.Ints(-200, 200), + initial_value=1, + ) + + for chan_name in ("A", "B", "C"): + channel = DummyChannel(f"{name}_Chan{chan_name}") self.add_submodule(chan_name, channel) def close(self) -> None: - for submodule in list(getattr(self, 'submodules', {}).values()): + for submodule in list(getattr(self, "submodules", {}).values()): try: submodule.close() except Exception: @@ -70,9 +79,9 @@ def close(self) -> None: def ask_raw(self, cmd): """Dummy ask_raw so *IDN? and similar SCPI queries don't explode the GUI.""" - if cmd.strip().upper().startswith('*IDN'): - return f'dummy,{self.name},0,0' - return '' + if cmd.strip().upper().startswith("*IDN"): + return f"dummy,{self.name},0,0" + return "" def test_func(self, a, b, *args, c: List[int] = [10, 11], **kwargs): """ @@ -82,22 +91,23 @@ def test_func(self, a, b, *args, c: List[int] = [10, 11], **kwargs): :param b: Even nicer parameter :param c: This one sucks though. """ - return a, b, args[0], c, kwargs['d'], self.param0() + return a, b, args[0], c, kwargs["d"], self.param0() def dummy_function(self, *args, **kwargs): """ Such a dumb dummy function here doing nothing other than printing and occupying your precious, precious terminal space. """ - print(f'the dummy chanel: {self.name} has been activated with:') - print(f'args: {args}') - print(f'kwargs: {kwargs}') + print(f"the dummy chanel: {self.name} has been activated with:") + print(f"args: {args}") + print(f"kwargs: {kwargs}") return self.address, self.first_arg, self.second_arg class DummyInstrumentTimeout(Instrument): """A dummy instrument to test timeout situations.""" - def __init__(self, name: str, *args, **kwargs): + + def __init__(self, name: str, *args, **kwargs): super().__init__(name, *args, **kwargs) self.random = np.random.randint(10000) @@ -105,16 +115,26 @@ def __init__(self, name: str, *args, **kwargs): self._param2 = 2 self._p1_get_counter = 0 - self.add_parameter('random_int', get_cmd=self.get_random) - self.add_parameter('param1', get_cmd= self._get_param1, set_cmd=lambda p: setattr(self, '_param1', p)) - self.add_parameter('param2', get_cmd=lambda : self._param2, set_cmd=lambda p: setattr(self, '_param2', p)) + self.add_parameter("random_int", get_cmd=self.get_random) + self.add_parameter( + "param1", + get_cmd=self._get_param1, + set_cmd=lambda p: setattr(self, "_param1", p), + ) + self.add_parameter( + "param2", + get_cmd=lambda: self._param2, + set_cmd=lambda p: setattr(self, "_param2", p), + ) def _get_param1(self): # for testing potentially redundant/duplicate get calls - print(f"-------------- getting {self.name}.param1, count {self._p1_get_counter}----------------") + print( + f"-------------- getting {self.name}.param1, count {self._p1_get_counter}----------------" + ) self._p1_get_counter += 1 return self._param1 - + def get_random(self): return self.random @@ -123,49 +143,43 @@ def get_random_timeout(self, wait_time=10): return self.get_random() - class DummyInstrumentRandomNumber(Instrument): """A dummy instrument with a few parameters that have random numbers generated on demand""" def __init__(self, name: str, *args, **kwargs): super().__init__(name, *args, **kwargs) - self.add_parameter('param0', - set_cmd=None, - vals=validators.Numbers(1, 10), - initial_value=1) + self.add_parameter( + "param0", set_cmd=None, vals=validators.Numbers(1, 10), initial_value=1 + ) - self.add_parameter('param1', - set_cmd=None, - vals=validators.Numbers(10, 20), - initial_value=10) + self.add_parameter( + "param1", set_cmd=None, vals=validators.Numbers(10, 20), initial_value=10 + ) - self.add_parameter('param2', - set_cmd=None, - vals=validators.Numbers(20, 30), - initial_value=20) + self.add_parameter( + "param2", set_cmd=None, vals=validators.Numbers(20, 30), initial_value=20 + ) - self.add_parameter('param3', - set_cmd=None, - vals=validators.Numbers(30, 40), - initial_value=30) + self.add_parameter( + "param3", set_cmd=None, vals=validators.Numbers(30, 40), initial_value=30 + ) - self.add_parameter('param4', - set_cmd=None, - vals=validators.Numbers(40, 50), - initial_value=40) + self.add_parameter( + "param4", set_cmd=None, vals=validators.Numbers(40, 50), initial_value=40 + ) def generate_data(self, name: str): - if name == 'param0': + if name == "param0": self.parameters[name].set(np.random.randint(1, 10)) - if name == 'param1': + if name == "param1": self.parameters[name].set(np.random.randint(10, 20)) - if name == 'param2': + if name == "param2": self.parameters[name].set(np.random.randint(20, 30)) - if name == 'param3': + if name == "param3": self.parameters[name].set(np.random.randint(30, 40)) - if name == 'param4': + if name == "param4": self.parameters[name].set(np.random.randint(40, 50)) def get(self, param_name): @@ -177,6 +191,7 @@ class FieldVectorIns(Instrument): """ class used to develop json serialization and guis """ + def __init__(self, name, starting_parameter=22, *args, **kwargs): super().__init__(name=name, *args, **kwargs) @@ -185,26 +200,29 @@ def __init__(self, name, starting_parameter=22, *args, **kwargs): self.complex_lst = [1 + 1j, -2 - 2j] self.starting_parameter = starting_parameter - self.add_parameter(name="field", - label='target field', - unit='T', - get_cmd=self.get_field, - set_cmd=self.set_field, - ) - - self.add_parameter(name='complex', - label='complex value', - unit='', - get_cmd=self.get_complex, - set_cmd=self.set_complex, - ) - - self.add_parameter(name='complex_list', - label='complex list', - unit='', - get_cmd=self.get_complex_list, - set_cmd=self.set_complex_list, - ) + self.add_parameter( + name="field", + label="target field", + unit="T", + get_cmd=self.get_field, + set_cmd=self.set_field, + ) + + self.add_parameter( + name="complex", + label="complex value", + unit="", + get_cmd=self.get_complex, + set_cmd=self.set_complex, + ) + + self.add_parameter( + name="complex_list", + label="complex list", + unit="", + get_cmd=self.get_complex_list, + set_cmd=self.set_complex_list, + ) def get_starting_parameter(self): return self.starting_parameter @@ -231,5 +249,5 @@ def set_complex_list(self, value): self.complex_lst = value def generic_function(self): - print(f'this generic function has been called') + print(f"this generic function has been called") return 3 diff --git a/src/instrumentserver/testing/dummy_instruments/rf.py b/src/instrumentserver/testing/dummy_instruments/rf.py index 4fc9d2a..cec96fa 100644 --- a/src/instrumentserver/testing/dummy_instruments/rf.py +++ b/src/instrumentserver/testing/dummy_instruments/rf.py @@ -5,7 +5,6 @@ from qcodes.utils import validators - class ResonatorResponse(Instrument): """A dummy instrument that generates the response of a resonator measured in reflection. @@ -21,46 +20,88 @@ def __init__(self, name, f0=5e9, df=1e6, **kw): self._frq_mod_multiply = False # add params of the resonator and the virtual detection chain - self.add_parameter('resonator_frequency', set_cmd=None, unit='Hz', - vals=validators.Numbers(1, 50e9), - initial_value=f0) - self.add_parameter('resonator_linewidth', set_cmd=None, unit='Hz', - vals=validators.Numbers(1, 1e9), - initial_value=df) - self.add_parameter('noise_temperature', set_cmd=None, unit='K', - vals=validators.Numbers(0.05, 3000), - initial_value=4.0) - self.add_parameter('input_attenuation', set_cmd=None, unit='dB', - vals=validators.Numbers(0, 200), - initial_value=70) + self.add_parameter( + "resonator_frequency", + set_cmd=None, + unit="Hz", + vals=validators.Numbers(1, 50e9), + initial_value=f0, + ) + self.add_parameter( + "resonator_linewidth", + set_cmd=None, + unit="Hz", + vals=validators.Numbers(1, 1e9), + initial_value=df, + ) + self.add_parameter( + "noise_temperature", + set_cmd=None, + unit="K", + vals=validators.Numbers(0.05, 3000), + initial_value=4.0, + ) + self.add_parameter( + "input_attenuation", + set_cmd=None, + unit="dB", + vals=validators.Numbers(0, 200), + initial_value=70, + ) # actual instrument parameters - self.add_parameter('start_frequency', set_cmd=None, unit='Hz', - vals=validators.Numbers(20e3, 19.999e9), - initial_value=20e3) - self.add_parameter('stop_frequency', set_cmd=None, unit='Hz', - vals=validators.Numbers(20.1e3, 20e9), - initial_value=20e9) - self.add_parameter('npoints', set_cmd=None, vals=validators.Ints(2, 40001), - initial_value=1601) - self.add_parameter('bandwidth', set_cmd=None, unit='Hz', - vals=validators.Numbers(1, 1e6), initial_value=10e3) - self.add_parameter('power', set_cmd=None, unit='dBm', - vals=validators.Numbers(-100, 0), initial_value=-100) + self.add_parameter( + "start_frequency", + set_cmd=None, + unit="Hz", + vals=validators.Numbers(20e3, 19.999e9), + initial_value=20e3, + ) + self.add_parameter( + "stop_frequency", + set_cmd=None, + unit="Hz", + vals=validators.Numbers(20.1e3, 20e9), + initial_value=20e9, + ) + self.add_parameter( + "npoints", set_cmd=None, vals=validators.Ints(2, 40001), initial_value=1601 + ) + self.add_parameter( + "bandwidth", + set_cmd=None, + unit="Hz", + vals=validators.Numbers(1, 1e6), + initial_value=10e3, + ) + self.add_parameter( + "power", + set_cmd=None, + unit="dBm", + vals=validators.Numbers(-100, 0), + initial_value=-100, + ) # data parameters - self.add_parameter('frequency', unit='Hz', - vals=validators.Arrays(shape=(self.npoints.get_latest,)), - get_cmd=self._frequency_vals, - snapshot_value=False, ) - self.add_parameter('data', - parameter_class=ParameterWithSetpoints, - setpoints=[self.frequency, ], - vals=validators.Arrays( - shape=(self.npoints.get_latest,), - valid_types=[np.complexfloating], - ), - get_cmd=self._get_data, ) + self.add_parameter( + "frequency", + unit="Hz", + vals=validators.Arrays(shape=(self.npoints.get_latest,)), + get_cmd=self._frequency_vals, + snapshot_value=False, + ) + self.add_parameter( + "data", + parameter_class=ParameterWithSetpoints, + setpoints=[ + self.frequency, + ], + vals=validators.Arrays( + shape=(self.npoints.get_latest,), + valid_types=[np.complexfloating], + ), + get_cmd=self._get_data, + ) def modulate_frequency(self, delta: float = 0, multiply=False) -> None: """Add an offset to the resonance frequency. @@ -73,7 +114,9 @@ def modulate_frequency(self, delta: float = 0, multiply=False) -> None: # private utility methods def _frequency_vals(self): - return np.linspace(self.start_frequency(), self.stop_frequency(), self.npoints()) + return np.linspace( + self.start_frequency(), self.stop_frequency(), self.npoints() + ) def _get_data(self): f0 = self.resonator_frequency() @@ -89,7 +132,8 @@ def _get_data(self): self.resonator_linewidth(), self.power() - self.input_attenuation(), self.bandwidth(), - self.noise_temperature()) + self.noise_temperature(), + ) return data @@ -109,7 +153,7 @@ def _resonator_reflection_signal(self, fvals, f0, df, P_in, BW, T_N): det = fvals - f0 pwr = 1e-3 * 10 ** (P_in / 10) # convert dBm to Watt ideal_signal = (2j * det - df) / (2j * det + df) - noise = (constants.k * T_N * BW / pwr) ** .5 + noise = (constants.k * T_N * BW / pwr) ** 0.5 noise_real = np.random.normal(size=ideal_signal.size, loc=0, scale=noise) noise_imag = np.random.normal(size=ideal_signal.size, loc=0, scale=noise) return ideal_signal + noise_real + 1j * noise_imag @@ -121,42 +165,55 @@ class Generator(Instrument): def __init__(self, name, *arg, **kw): super().__init__(name, *arg, **kw) - self.add_parameter('frequency', unit='Hz', - set_cmd=None, - vals=validators.Numbers(1e3, 20e9), - initial_value=10e9) + self.add_parameter( + "frequency", + unit="Hz", + set_cmd=None, + vals=validators.Numbers(1e3, 20e9), + initial_value=10e9, + ) - self.add_parameter('power', unit='dBm', - set_cmd=None, - vals=validators.Numbers(-100, 25), - initial_value=-100) + self.add_parameter( + "power", + unit="dBm", + set_cmd=None, + vals=validators.Numbers(-100, 25), + initial_value=-100, + ) - self.add_parameter('rf_on', set_cmd=None, - vals=validators.Bool(), - initial_value=False) + self.add_parameter( + "rf_on", set_cmd=None, vals=validators.Bool(), initial_value=False + ) class FluxControl(Instrument): """A dummy that hooks to :class:`.ResonatorResponse` and modifies its resonance frequency as if the resonator were a squid.""" - def __init__(self, name: str, resonator_instrument: str, - *args, **kwargs): + def __init__(self, name: str, resonator_instrument: str, *args, **kwargs): super().__init__(name, *args, **kwargs) - self._resonator = find_or_create_instrument(instrument_class=ResonatorResponse, - name=resonator_instrument) - - self.add_parameter('inductive_participation_ratio', - set_cmd=None, - vals=validators.Numbers(0, 1), - initial_value=0.05) - - self.add_parameter('flux', unit='Phi_0', - set_cmd=self._set_flux, - vals=validators.Numbers(-1, 1), - initial_value=0) + self._resonator = find_or_create_instrument( + instrument_class=ResonatorResponse, name=resonator_instrument + ) + + self.add_parameter( + "inductive_participation_ratio", + set_cmd=None, + vals=validators.Numbers(0, 1), + initial_value=0.05, + ) + + self.add_parameter( + "flux", + unit="Phi_0", + set_cmd=self._set_flux, + vals=validators.Numbers(-1, 1), + initial_value=0, + ) def _set_flux(self, flux): - mod = 1./(1. + self.inductive_participation_ratio() / np.abs(np.cos(np.pi*flux))) + mod = 1.0 / ( + 1.0 + self.inductive_participation_ratio() / np.abs(np.cos(np.pi * flux)) + ) self._resonator.modulate_frequency(mod, True) diff --git a/test/client_workingdir/init_client.py b/test/client_workingdir/init_client.py index fe06c22..41d111d 100755 --- a/test/client_workingdir/init_client.py +++ b/test/client_workingdir/init_client.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -#%% Imports +# %% Imports import os from qcodes import Instrument @@ -8,32 +8,30 @@ from instrumentserver.client import ProxyInstrument -#%% Create all my instruments +# %% Create all my instruments Instrument.close_all() ins_cli = Client() dummy_vna = ins_cli.find_or_create_instrument( - 'dummy_vna', - 'instrumentserver.testing.dummy_instruments.rf.ResonatorResponse', - + "dummy_vna", + "instrumentserver.testing.dummy_instruments.rf.ResonatorResponse", ) dummy_multichan = ins_cli.find_or_create_instrument( - 'dummy_multichan', - 'instrumentserver.testing.dummy_instruments.generic.DummyInstrumentWithSubmodule', - + "dummy_multichan", + "instrumentserver.testing.dummy_instruments.generic.DummyInstrumentWithSubmodule", ) pm = ins_cli.find_or_create_instrument( - 'pm', - 'instrumentserver.params.ParameterManager', + "pm", + "instrumentserver.params.ParameterManager", ) -#%% save the state +# %% save the state # Note: This now saves ALL instruments' parameters, not just pm -ins_cli.paramsToFile(os.path.abspath('./parameters.json')) +ins_cli.paramsToFile(os.path.abspath("./parameters.json")) -#%% load pm settings from file +# %% load pm settings from file # Note: This now loads ALL instruments' parameters from file -ins_cli.paramsFromFile(os.path.abspath('./parameters.json')) +ins_cli.paramsFromFile(os.path.abspath("./parameters.json")) diff --git a/test/notebooks/Autoupdate.ipynb b/test/notebooks/Autoupdate.ipynb index 3e3f937..2f8bcac 100644 --- a/test/notebooks/Autoupdate.ipynb +++ b/test/notebooks/Autoupdate.ipynb @@ -27,7 +27,8 @@ "outputs": [], "source": [ "from instrumentserver.client import Client as InstrumentClient\n", - "cli = InstrumentClient() # connect to default host (localhost) and default port (5555)" + "\n", + "cli = InstrumentClient() # connect to default host (localhost) and default port (5555)" ] }, { @@ -85,7 +86,9 @@ } ], "source": [ - "params = cli.get_instrument('params') # 'params' is the name the startup script gave the instrument\n", + "params = cli.get_instrument(\n", + " \"params\"\n", + ") # 'params' is the name the startup script gave the instrument\n", "\n", "# simply output the value of the pi pulse length:\n", "params.qubit.pipulse.len()" @@ -137,17 +140,17 @@ "evalue": "'ProxyInstrumentModule' object and its delegates have no attribute 'test'", "output_type": "error", "traceback": [ - "\u001B[1;31m---------------------------------------------------------------------------\u001B[0m", - "\u001B[1;31mAttributeError\u001B[0m Traceback (most recent call last)", - "\u001B[1;32mc:\\users\\msmt\\documents\\github\\instrumentserver\\instrumentserver\\client\\proxy.py\u001B[0m in \u001B[0;36m__getattr__\u001B[1;34m(self, item)\u001B[0m\n\u001B[0;32m 326\u001B[0m \u001B[1;32mtry\u001B[0m\u001B[1;33m:\u001B[0m\u001B[1;33m\u001B[0m\u001B[1;33m\u001B[0m\u001B[0m\n\u001B[1;32m--> 327\u001B[1;33m \u001B[1;32mreturn\u001B[0m \u001B[0msuper\u001B[0m\u001B[1;33m(\u001B[0m\u001B[1;33m)\u001B[0m\u001B[1;33m.\u001B[0m\u001B[0m__getattr__\u001B[0m\u001B[1;33m(\u001B[0m\u001B[0mitem\u001B[0m\u001B[1;33m)\u001B[0m\u001B[1;33m\u001B[0m\u001B[1;33m\u001B[0m\u001B[0m\n\u001B[0m\u001B[0;32m 328\u001B[0m \u001B[1;32mexcept\u001B[0m \u001B[0mException\u001B[0m \u001B[1;32mas\u001B[0m \u001B[0me\u001B[0m\u001B[1;33m:\u001B[0m\u001B[1;33m\u001B[0m\u001B[1;33m\u001B[0m\u001B[0m\n", - "\u001B[1;32m~\\Miniconda3\\envs\\qcodes\\lib\\site-packages\\qcodes\\utils\\helpers.py\u001B[0m in \u001B[0;36m__getattr__\u001B[1;34m(self, key)\u001B[0m\n\u001B[0;32m 405\u001B[0m \u001B[1;33m\u001B[0m\u001B[0m\n\u001B[1;32m--> 406\u001B[1;33m raise AttributeError(\n\u001B[0m\u001B[0;32m 407\u001B[0m \"'{}' object and its delegates have no attribute '{}'\".format(\n", - "\u001B[1;31mAttributeError\u001B[0m: 'ProxyInstrumentModule' object and its delegates have no attribute 'test'", + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mAttributeError\u001b[0m Traceback (most recent call last)", + "\u001b[1;32mc:\\users\\msmt\\documents\\github\\instrumentserver\\instrumentserver\\client\\proxy.py\u001b[0m in \u001b[0;36m__getattr__\u001b[1;34m(self, item)\u001b[0m\n\u001b[0;32m 326\u001b[0m \u001b[1;32mtry\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 327\u001b[1;33m \u001b[1;32mreturn\u001b[0m \u001b[0msuper\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m__getattr__\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mitem\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 328\u001b[0m \u001b[1;32mexcept\u001b[0m \u001b[0mException\u001b[0m \u001b[1;32mas\u001b[0m \u001b[0me\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32m~\\Miniconda3\\envs\\qcodes\\lib\\site-packages\\qcodes\\utils\\helpers.py\u001b[0m in \u001b[0;36m__getattr__\u001b[1;34m(self, key)\u001b[0m\n\u001b[0;32m 405\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 406\u001b[1;33m raise AttributeError(\n\u001b[0m\u001b[0;32m 407\u001b[0m \"'{}' object and its delegates have no attribute '{}'\".format(\n", + "\u001b[1;31mAttributeError\u001b[0m: 'ProxyInstrumentModule' object and its delegates have no attribute 'test'", "\nDuring handling of the above exception, another exception occurred:\n", - "\u001B[1;31mAttributeError\u001B[0m Traceback (most recent call last)", - "\u001B[1;32m\u001B[0m in \u001B[0;36m\u001B[1;34m\u001B[0m\n\u001B[1;32m----> 1\u001B[1;33m \u001B[0mparams\u001B[0m\u001B[1;33m.\u001B[0m\u001B[0mqubit\u001B[0m\u001B[1;33m.\u001B[0m\u001B[0mtest\u001B[0m\u001B[1;33m(\u001B[0m\u001B[1;33m)\u001B[0m\u001B[1;33m\u001B[0m\u001B[1;33m\u001B[0m\u001B[0m\n\u001B[0m", - "\u001B[1;32mc:\\users\\msmt\\documents\\github\\instrumentserver\\instrumentserver\\client\\proxy.py\u001B[0m in \u001B[0;36m__getattr__\u001B[1;34m(self, item)\u001B[0m\n\u001B[0;32m 328\u001B[0m \u001B[1;32mexcept\u001B[0m \u001B[0mException\u001B[0m \u001B[1;32mas\u001B[0m \u001B[0me\u001B[0m\u001B[1;33m:\u001B[0m\u001B[1;33m\u001B[0m\u001B[1;33m\u001B[0m\u001B[0m\n\u001B[0;32m 329\u001B[0m \u001B[0mprint\u001B[0m\u001B[1;33m(\u001B[0m\u001B[1;34mf\"{type(e)}: {e.args}\"\u001B[0m\u001B[1;33m)\u001B[0m\u001B[1;33m\u001B[0m\u001B[1;33m\u001B[0m\u001B[0m\n\u001B[1;32m--> 330\u001B[1;33m \u001B[1;32mreturn\u001B[0m \u001B[0msuper\u001B[0m\u001B[1;33m(\u001B[0m\u001B[1;33m)\u001B[0m\u001B[1;33m.\u001B[0m\u001B[0m__getattr__\u001B[0m\u001B[1;33m(\u001B[0m\u001B[0mitem\u001B[0m\u001B[1;33m)\u001B[0m\u001B[1;33m\u001B[0m\u001B[1;33m\u001B[0m\u001B[0m\n\u001B[0m\u001B[0;32m 331\u001B[0m \u001B[1;33m\u001B[0m\u001B[0m\n\u001B[0;32m 332\u001B[0m \u001B[1;33m\u001B[0m\u001B[0m\n", - "\u001B[1;32m~\\Miniconda3\\envs\\qcodes\\lib\\site-packages\\qcodes\\utils\\helpers.py\u001B[0m in \u001B[0;36m__getattr__\u001B[1;34m(self, key)\u001B[0m\n\u001B[0;32m 404\u001B[0m \u001B[1;32mpass\u001B[0m\u001B[1;33m\u001B[0m\u001B[1;33m\u001B[0m\u001B[0m\n\u001B[0;32m 405\u001B[0m \u001B[1;33m\u001B[0m\u001B[0m\n\u001B[1;32m--> 406\u001B[1;33m raise AttributeError(\n\u001B[0m\u001B[0;32m 407\u001B[0m \"'{}' object and its delegates have no attribute '{}'\".format(\n\u001B[0;32m 408\u001B[0m self.__class__.__name__, key))\n", - "\u001B[1;31mAttributeError\u001B[0m: 'ProxyInstrumentModule' object and its delegates have no attribute 'test'" + "\u001b[1;31mAttributeError\u001b[0m Traceback (most recent call last)", + "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[0mparams\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mqubit\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mtest\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[1;32mc:\\users\\msmt\\documents\\github\\instrumentserver\\instrumentserver\\client\\proxy.py\u001b[0m in \u001b[0;36m__getattr__\u001b[1;34m(self, item)\u001b[0m\n\u001b[0;32m 328\u001b[0m \u001b[1;32mexcept\u001b[0m \u001b[0mException\u001b[0m \u001b[1;32mas\u001b[0m \u001b[0me\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 329\u001b[0m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34mf\"{type(e)}: {e.args}\"\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 330\u001b[1;33m \u001b[1;32mreturn\u001b[0m \u001b[0msuper\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m__getattr__\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mitem\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 331\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 332\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32m~\\Miniconda3\\envs\\qcodes\\lib\\site-packages\\qcodes\\utils\\helpers.py\u001b[0m in \u001b[0;36m__getattr__\u001b[1;34m(self, key)\u001b[0m\n\u001b[0;32m 404\u001b[0m \u001b[1;32mpass\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 405\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 406\u001b[1;33m raise AttributeError(\n\u001b[0m\u001b[0;32m 407\u001b[0m \"'{}' object and its delegates have no attribute '{}'\".format(\n\u001b[0;32m 408\u001b[0m self.__class__.__name__, key))\n", + "\u001b[1;31mAttributeError\u001b[0m: 'ProxyInstrumentModule' object and its delegates have no attribute 'test'" ] } ], @@ -200,7 +203,7 @@ "class MyData:\n", " name: str\n", " value: float\n", - " \n", + "\n", " def __repr__(self):\n", " return str(self)\n", "\n", @@ -209,7 +212,7 @@ " 'name': {self.name},\n", " 'value': {self.value},\n", "}}\n", - "\"\"\"\n" + "\"\"\"" ] }, { @@ -219,7 +222,7 @@ "metadata": {}, "outputs": [], "source": [ - "data = MyData(name='my name', value=1.23)" + "data = MyData(name=\"my name\", value=1.23)" ] }, { @@ -317,7 +320,7 @@ "metadata": {}, "outputs": [], "source": [ - "socket.setsockopt_string(zmq.SUBSCRIBE, '')\n", + "socket.setsockopt_string(zmq.SUBSCRIBE, \"\")\n", "\n", "msg = socket.recv_string()\n", "print(msg)" @@ -366,20 +369,21 @@ "metadata": {}, "outputs": [], "source": [ - "class MyClass: \n", + "class MyClass:\n", " x = 10\n", " y = 20\n", - " \n", + "\n", " def __getattr__(self, name):\n", - " print(f'gotta set {name}!')\n", + " print(f\"gotta set {name}!\")\n", " setattr(self, name, True)\n", " return True\n", "\n", + "\n", "# if not hasattr(self, name):\n", "# print(f'gotta create {name}!')\n", "# setattr(self, name, True)\n", "# return True\n", - " \n", + "\n", "c = MyClass()" ] }, diff --git a/test/notebooks/Prototype the ParamManager.ipynb b/test/notebooks/Prototype the ParamManager.ipynb index f6dafdb..0953edd 100644 --- a/test/notebooks/Prototype the ParamManager.ipynb +++ b/test/notebooks/Prototype the ParamManager.ipynb @@ -42,7 +42,9 @@ "from instrumentserver import setupLogging, servergui\n", "from instrumentserver.helpers import getInstrumentMethods, getInstrumentParameters\n", "from instrumentserver.serialize import (\n", - " toDataFrame, saveParamsToFile, loadParamsFromFile,\n", + " toDataFrame,\n", + " saveParamsToFile,\n", + " loadParamsFromFile,\n", " toParamDict,\n", ")\n", "\n", @@ -70,23 +72,23 @@ "source": [ "Instrument.close_all()\n", "\n", - "pm = ParameterManager('pm')\n", + "pm = ParameterManager(\"pm\")\n", "station = Station(pm)\n", "\n", - "pm.add('sample_name', 'qubit_test-5', vals=validators.Strings())\n", + "pm.add(\"sample_name\", \"qubit_test-5\", vals=validators.Strings())\n", "\n", - "pm.add('readout.pulse_length', 1000, unit='ns', vals=validators.Ints())\n", - "pm.add('readout.envelope', 'envelope_file.npz', vals=validators.Strings())\n", - "pm.add('readout.n_repetitions', 1000, vals=validators.Ints())\n", - "pm.add('readout.use_envelope', True, vals=validators.Bool())\n", + "pm.add(\"readout.pulse_length\", 1000, unit=\"ns\", vals=validators.Ints())\n", + "pm.add(\"readout.envelope\", \"envelope_file.npz\", vals=validators.Strings())\n", + "pm.add(\"readout.n_repetitions\", 1000, vals=validators.Ints())\n", + "pm.add(\"readout.use_envelope\", True, vals=validators.Bool())\n", "\n", - "pm.add('qubit.frequency', 5.678e9, unit='Hz', vals=validators.Numbers())\n", - "pm.add('qubit.pi_pulse.len', 20, unit='ns', vals=validators.Ints())\n", - "pm.add('qubit.pi_pulse.amp', 126, unit='DAC units', vals=validators.Ints())\n", + "pm.add(\"qubit.frequency\", 5.678e9, unit=\"Hz\", vals=validators.Numbers())\n", + "pm.add(\"qubit.pi_pulse.len\", 20, unit=\"ns\", vals=validators.Ints())\n", + "pm.add(\"qubit.pi_pulse.amp\", 126, unit=\"DAC units\", vals=validators.Ints())\n", "\n", - "pm.add('morestuff.a_sequence', [])\n", - "pm.add('morestuff.a_complex_number', 0+0j, vals=validators.ComplexNumbers())\n", - "pm.add('morestuff.something.hidden.deep.away', True, vals=validators.Bool())" + "pm.add(\"morestuff.a_sequence\", [])\n", + "pm.add(\"morestuff.a_complex_number\", 0 + 0j, vals=validators.ComplexNumbers())\n", + "pm.add(\"morestuff.something.hidden.deep.away\", True, vals=validators.Bool())" ] }, { @@ -97,9 +99,7 @@ }, "outputs": [], "source": [ - "dialog = widgetDialog(\n", - " ParameterManagerGui(pm)\n", - ")" + "dialog = widgetDialog(ParameterManagerGui(pm))" ] }, { @@ -285,7 +285,7 @@ }, "outputs": [], "source": [ - "pm.add('morestuff.something.else', 5)\n", + "pm.add(\"morestuff.something.else\", 5)\n", "pm.qubit.pi_pulse.len(40)" ] }, diff --git a/test/notebooks/Run the station server from a notebook.ipynb b/test/notebooks/Run the station server from a notebook.ipynb index 31e2114..9ecbdd1 100644 --- a/test/notebooks/Run the station server from a notebook.ipynb +++ b/test/notebooks/Run the station server from a notebook.ipynb @@ -47,16 +47,18 @@ "\n", "# dummy instrument: VNA\n", "from instrumentserver.testing.dummy_instruments.rf import ResonatorResponse\n", - "dummy_vna = ResonatorResponse('dummy_vna')\n", + "\n", + "dummy_vna = ResonatorResponse(\"dummy_vna\")\n", "dummy_vna.start_frequency(4.9e9)\n", "dummy_vna.stop_frequency(5.1e9)\n", "\n", "from instrumentserver.testing.dummy_instruments.rf import Generator\n", - "rf_src = Generator('rf_src')\n", - "lo_src = Generator('lo_src')\n", - "qubit_src = Generator('qubit_src')\n", "\n", - "current_sample = Parameter('current_sample', set_cmd=None, initial_value='testsample')\n", + "rf_src = Generator(\"rf_src\")\n", + "lo_src = Generator(\"lo_src\")\n", + "qubit_src = Generator(\"qubit_src\")\n", + "\n", + "current_sample = Parameter(\"current_sample\", set_cmd=None, initial_value=\"testsample\")\n", "\n", "station = Station(dummy_vna, current_sample, rf_src, lo_src, qubit_src)\n", "\n", diff --git a/test/prototyping/server_admin.py b/test/prototyping/server_admin.py index 38dabb1..3885983 100755 --- a/test/prototyping/server_admin.py +++ b/test/prototyping/server_admin.py @@ -2,7 +2,7 @@ import logging -#%% imports +# %% imports from qcodes import Instrument from instrumentserver.server import * from instrumentserver.client import * @@ -14,42 +14,41 @@ # logger.setLevel(logging.DEBUG) -#%% shut down the server +# %% shut down the server with Client() as cli: - cli.ask('SHUTDOWN') + cli.ask("SHUTDOWN") -#%% create vna instrument in server +# %% create vna instrument in server Instrument.close_all() ins_cli = Client() dummy_vna = ins_cli.find_or_create_instrument( - 'dummy_vna', - 'instrumentserver.testing.dummy_instruments.rf.ResonatorResponse', - + "dummy_vna", + "instrumentserver.testing.dummy_instruments.rf.ResonatorResponse", ) dummy_multichan = ins_cli.find_or_create_instrument( - 'dummy_multichan', - 'instrumentserver.testing.dummy_instruments.generic.DummyInstrumentWithSubmodule', + "dummy_multichan", + "instrumentserver.testing.dummy_instruments.generic.DummyInstrumentWithSubmodule", ) pm = ins_cli.find_or_create_instrument( - 'pm', - 'instrumentserver.params.ParameterManager', + "pm", + "instrumentserver.params.ParameterManager", ) -#%% Close an instrument +# %% Close an instrument with Client() as cli: - cli.close_instrument('dummy_vna') + cli.close_instrument("dummy_vna") -#%% get instruments from server +# %% get instruments from server with Client() as cli: pprint(cli.list_instruments()) -#%% get the snapshot from the station +# %% get the snapshot from the station with Client() as cli: snap = cli.get_snapshot() pprint(snap) diff --git a/test/prototyping/testing_parameter_manager.py b/test/prototyping/testing_parameter_manager.py index 0de676a..5d669ce 100755 --- a/test/prototyping/testing_parameter_manager.py +++ b/test/prototyping/testing_parameter_manager.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -#%% imports +# %% imports import inspect import numpy as np @@ -15,18 +15,17 @@ from instrumentserver.client import Client, ProxyInstrument - -#%% run the PM locally +# %% run the PM locally Instrument.close_all() -pm = ParameterManager('pm') +pm = ParameterManager("pm") station = Station() station.add_component(pm) dialog = widgetDialog(ParameterManagerGui(pm)) -#%% instantiate PM in the server. +# %% instantiate PM in the server. Instrument.close_all() cli = Client() -pm2 = ProxyInstrument('pm', cli=cli, remotePath='pm') -dialog = widgetDialog(ParameterManagerGui(pm2)) \ No newline at end of file +pm2 = ProxyInstrument("pm", cli=cli, remotePath="pm") +dialog = widgetDialog(ParameterManagerGui(pm2)) diff --git a/test/pytest/conftest.py b/test/pytest/conftest.py index 9667a65..6aa83d6 100644 --- a/test/pytest/conftest.py +++ b/test/pytest/conftest.py @@ -7,7 +7,7 @@ from instrumentserver.client.proxy import Client -@pytest.fixture(autouse=True, scope='module') +@pytest.fixture(autouse=True, scope="module") def _close_instruments_between_modules(): """Ensure every test module starts with a clean qcodes instrument registry. @@ -21,7 +21,7 @@ def _close_instruments_between_modules(): qc.Instrument.close_all() -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def qapp_session(): """Ensure a QApplication exists for the entire test session. @@ -30,13 +30,14 @@ def qapp_session(): This fixture guarantees the app exists even for non-GUI tests. """ from instrumentserver import QtWidgets + app = QtWidgets.QApplication.instance() if app is None: app = QtWidgets.QApplication([]) return app -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def start_server(qapp_session): server, thread = startServer() yield server @@ -61,11 +62,16 @@ def cli(start_server): @pytest.fixture() def dummy_instrument(cli): - dummy = cli.find_or_create_instrument('dummy', 'instrumentserver.testing.dummy_instruments.generic.DummyInstrumentWithSubmodule') + dummy = cli.find_or_create_instrument( + "dummy", + "instrumentserver.testing.dummy_instruments.generic.DummyInstrumentWithSubmodule", + ) return cli, dummy @pytest.fixture() def param_manager(cli): - params = cli.find_or_create_instrument('parameter_manager', 'instrumentserver.params.ParameterManager') + params = cli.find_or_create_instrument( + "parameter_manager", "instrumentserver.params.ParameterManager" + ) return cli, params diff --git a/test/pytest/test_base.py b/test/pytest/test_base.py index e98285c..54e26f4 100644 --- a/test/pytest/test_base.py +++ b/test/pytest/test_base.py @@ -3,12 +3,20 @@ Note: encode/decode in this module is designed to work with blueprint objects that implement .toJson() — it is not a general-purpose JSON encoder. """ + import time import pytest import zmq -from instrumentserver.base import encode, decode, send, recv, sendBroadcast, recvMultipart +from instrumentserver.base import ( + encode, + decode, + send, + recv, + sendBroadcast, + recvMultipart, +) from instrumentserver.blueprints import ( ParameterBroadcastBluePrint, ServerInstruction, @@ -22,21 +30,26 @@ # encode / decode (blueprint objects only) # --------------------------------------------------------------------------- + def test_encode_broadcast_blueprint_returns_string(): - bp = ParameterBroadcastBluePrint(name='p', action='parameter-update', value=42, unit='V') + bp = ParameterBroadcastBluePrint( + name="p", action="parameter-update", value=42, unit="V" + ) result = encode(bp) assert isinstance(result, str) def test_encode_decode_broadcast_blueprint_round_trip(): - bp = ParameterBroadcastBluePrint(name='p', action='parameter-update', value=42, unit='V') + bp = ParameterBroadcastBluePrint( + name="p", action="parameter-update", value=42, unit="V" + ) encoded = encode(bp) decoded = decode(encoded) assert isinstance(decoded, ParameterBroadcastBluePrint) - assert decoded.name == 'p' + assert decoded.name == "p" assert decoded.value == 42 - assert decoded.unit == 'V' - assert decoded.action == 'parameter-update' + assert decoded.unit == "V" + assert decoded.action == "parameter-update" def test_encode_decode_server_instruction_round_trip(): @@ -61,13 +74,14 @@ def test_decode_string_is_returned_as_is(): decoded = decode(encoded) # decoded is the original string (json.loads unwraps, deserialize_obj tries # to parse it as JSON again and returns the inner dict) - assert decoded == {'key': 'value'} + assert decoded == {"key": "value"} # --------------------------------------------------------------------------- # send / recv (using zmq PAIR sockets with blueprint objects) # --------------------------------------------------------------------------- + @pytest.fixture def zmq_pair(): ctx = zmq.Context() @@ -86,11 +100,13 @@ def zmq_pair(): def test_send_recv_broadcast_blueprint(zmq_pair): s1, s2 = zmq_pair - bp = ParameterBroadcastBluePrint(name='my_p', action='parameter-update', value=7, unit='Hz') + bp = ParameterBroadcastBluePrint( + name="my_p", action="parameter-update", value=7, unit="Hz" + ) send(s1, bp) result = recv(s2) assert isinstance(result, ParameterBroadcastBluePrint) - assert result.name == 'my_p' + assert result.name == "my_p" assert result.value == 7 @@ -111,6 +127,7 @@ def test_send_recv_server_instruction(zmq_pair): # sendBroadcast / recvMultipart (PUB/SUB) # --------------------------------------------------------------------------- + @pytest.fixture def zmq_pub_sub(): ctx = zmq.Context() @@ -118,7 +135,7 @@ def zmq_pub_sub(): sub = ctx.socket(zmq.SUB) port = pub.bind_to_random_port("tcp://127.0.0.1") sub.connect(f"tcp://127.0.0.1:{port}") - sub.setsockopt_string(zmq.SUBSCRIBE, '') + sub.setsockopt_string(zmq.SUBSCRIBE, "") sub.setsockopt(zmq.RCVTIMEO, 2000) pub.setsockopt(zmq.LINGER, 0) sub.setsockopt(zmq.LINGER, 0) @@ -131,17 +148,19 @@ def zmq_pub_sub(): def test_sendBroadcast_recvMultipart(zmq_pub_sub): pub, sub = zmq_pub_sub - bp = ParameterBroadcastBluePrint(name='my_param', action='parameter-update', value=7, unit='V') - sendBroadcast(pub, 'my_param', bp) + bp = ParameterBroadcastBluePrint( + name="my_param", action="parameter-update", value=7, unit="V" + ) + sendBroadcast(pub, "my_param", bp) name, result = recvMultipart(sub) - assert name == 'my_param' + assert name == "my_param" assert isinstance(result, ParameterBroadcastBluePrint) assert result.value == 7 def test_sendBroadcast_name_prefix_matches(zmq_pub_sub): pub, sub = zmq_pub_sub - bp = ParameterBroadcastBluePrint(name='ins.param', action='parameter-set', value=42) - sendBroadcast(pub, 'ins.param', bp) + bp = ParameterBroadcastBluePrint(name="ins.param", action="parameter-set", value=42) + sendBroadcast(pub, "ins.param", bp) name, result = recvMultipart(sub) - assert name == 'ins.param' + assert name == "ins.param" diff --git a/test/pytest/test_basic_functionality.py b/test/pytest/test_basic_functionality.py index 5215b10..9855209 100644 --- a/test/pytest/test_basic_functionality.py +++ b/test/pytest/test_basic_functionality.py @@ -3,20 +3,24 @@ def test_creating_and_accessing_param(param_manager): cli, params = param_manager - params.add_parameter(name='my_param', initial_value=123, unit='M') + params.add_parameter(name="my_param", initial_value=123, unit="M") assert params.my_param() == 123 - assert params.my_param.unit == 'M' + assert params.my_param.unit == "M" params.my_param(456) assert params.my_param() == 456 def test_getting_all_instruments(cli): - dummy = cli.find_or_create_instrument('dummy', - 'instrumentserver.testing.dummy_instruments.generic.DummyInstrumentWithSubmodule') - params = cli.find_or_create_instrument('parameter_manager', 'instrumentserver.params.ParameterManager') + dummy = cli.find_or_create_instrument( + "dummy", + "instrumentserver.testing.dummy_instruments.generic.DummyInstrumentWithSubmodule", + ) + params = cli.find_or_create_instrument( + "parameter_manager", "instrumentserver.params.ParameterManager" + ) all_ins = cli.list_instruments() - expected_dict = ['dummy', 'parameter_manager'] + expected_dict = ["dummy", "parameter_manager"] assert sorted(all_ins) == sorted(expected_dict) @@ -24,7 +28,7 @@ def test_calling_instrument_method(dummy_instrument): cli, ins = dummy_instrument ins.param0(1) assert ins.param0() == 1 - ret = ins.test_func(1, 2, 3, 4, c=[5, 6], d=False, more='hello') + ret = ins.test_func(1, 2, 3, 4, c=[5, 6], d=False, more="hello") expected = [1, 2, 3, [5, 6], False, ins.param0()] assert expected == ret @@ -32,13 +36,16 @@ def test_calling_instrument_method(dummy_instrument): def test_closing_instruments(dummy_instrument): cli, dummy = dummy_instrument - assert 'dummy' in cli.list_instruments() + assert "dummy" in cli.list_instruments() cli.close_instrument(dummy.name) - assert 'dummy' not in cli.list_instruments() + assert "dummy" not in cli.list_instruments() def test_sending_and_receiving_arbitrary_objects(cli): - magnet = cli.find_or_create_instrument(name='magnet', instrument_class='instrumentserver.testing.dummy_instruments.generic.FieldVectorIns') + magnet = cli.find_or_create_instrument( + name="magnet", + instrument_class="instrumentserver.testing.dummy_instruments.generic.FieldVectorIns", + ) # Testing receiving arbitrary return from method field_vector = magnet.get_field() @@ -60,4 +67,3 @@ def test_sending_and_receiving_arbitrary_objects(cli): new_field_vector = FieldVector(101, 102, 103) magnet.set_field(new_field_vector) assert magnet.get_field().is_equal(new_field_vector) - diff --git a/test/pytest/test_client_station.py b/test/pytest/test_client_station.py index f9b9ecc..da10e7f 100644 --- a/test/pytest/test_client_station.py +++ b/test/pytest/test_client_station.py @@ -1,4 +1,5 @@ """Tests for ClientStation and ClientStationGui.""" + import json from pathlib import Path @@ -6,44 +7,47 @@ from instrumentserver.client.proxy import ClientStation -DUMMY_CLASS = 'instrumentserver.testing.dummy_instruments.generic.DummyInstrumentWithSubmodule' +DUMMY_CLASS = ( + "instrumentserver.testing.dummy_instruments.generic.DummyInstrumentWithSubmodule" +) # --------------------------------------------------------------------------- # ClientStation (no GUI) # --------------------------------------------------------------------------- -@pytest.fixture(scope='module') + +@pytest.fixture(scope="module") def client_station(start_server): - station = ClientStation(host='localhost', port=5555) + station = ClientStation(host="localhost", port=5555) yield station station.disconnect() def test_client_station_creates_instruments(client_station): - ins = client_station.find_or_create_instrument('cs_dummy', DUMMY_CLASS) - assert 'cs_dummy' in client_station.instruments + ins = client_station.find_or_create_instrument("cs_dummy", DUMMY_CLASS) + assert "cs_dummy" in client_station.instruments def test_client_station_get_parameters(client_station): - client_station.find_or_create_instrument('cs_dummy', DUMMY_CLASS) + client_station.find_or_create_instrument("cs_dummy", DUMMY_CLASS) params = client_station.get_parameters() assert isinstance(params, dict) - assert 'cs_dummy' in params + assert "cs_dummy" in params def test_client_station_set_parameters(client_station): - ins = client_station.find_or_create_instrument('cs_dummy', DUMMY_CLASS) + ins = client_station.find_or_create_instrument("cs_dummy", DUMMY_CLASS) ins.param0(0) - client_station.set_parameters({'cs_dummy': {'param0': 1}}) + client_station.set_parameters({"cs_dummy": {"param0": 1}}) assert ins.param0() == 1 def test_client_station_save_load_parameters(tmp_path, client_station): - ins = client_station.find_or_create_instrument('cs_dummy', DUMMY_CLASS) + ins = client_station.find_or_create_instrument("cs_dummy", DUMMY_CLASS) ins.param0(1) - file_path = str(tmp_path / 'params.json') + file_path = str(tmp_path / "params.json") client_station.save_parameters(file_path) # Mutate the value @@ -56,24 +60,26 @@ def test_client_station_save_load_parameters(tmp_path, client_station): def test_client_station_get_instrument(client_station): - client_station.find_or_create_instrument('cs_dummy', DUMMY_CLASS) - retrieved = client_station.get_instrument('cs_dummy') + client_station.find_or_create_instrument("cs_dummy", DUMMY_CLASS) + retrieved = client_station.get_instrument("cs_dummy") assert retrieved is not None - assert retrieved.name == 'cs_dummy' + assert retrieved.name == "cs_dummy" def test_client_station_subscript_access(client_station): - client_station.find_or_create_instrument('cs_dummy', DUMMY_CLASS) - assert client_station['cs_dummy'] is client_station.instruments['cs_dummy'] + client_station.find_or_create_instrument("cs_dummy", DUMMY_CLASS) + assert client_station["cs_dummy"] is client_station.instruments["cs_dummy"] # --------------------------------------------------------------------------- # ClientStationGui # --------------------------------------------------------------------------- + def test_client_station_gui_opens(qtbot, start_server): from instrumentserver.client.application import ClientStationGui - station = ClientStation(host='localhost', port=5555) + + station = ClientStation(host="localhost", port=5555) window = ClientStationGui(station) qtbot.addWidget(window) try: @@ -85,14 +91,15 @@ def test_client_station_gui_opens(qtbot, start_server): def test_client_station_gui_has_three_tabs(qtbot, start_server): from instrumentserver.client.application import ClientStationGui - station = ClientStation(host='localhost', port=5555) + + station = ClientStation(host="localhost", port=5555) window = ClientStationGui(station) qtbot.addWidget(window) try: tab_texts = [window.tabs.tabText(i) for i in range(window.tabs.count())] - assert 'Station' in tab_texts - assert 'Log' in tab_texts - assert 'Server' in tab_texts + assert "Station" in tab_texts + assert "Log" in tab_texts + assert "Server" in tab_texts finally: window.close() station.disconnect() @@ -100,12 +107,13 @@ def test_client_station_gui_has_three_tabs(qtbot, start_server): def test_client_station_gui_server_widget_shows_host_port(qtbot, start_server): from instrumentserver.client.application import ClientStationGui - station = ClientStation(host='localhost', port=5555) + + station = ClientStation(host="localhost", port=5555) window = ClientStationGui(station) qtbot.addWidget(window) try: - assert window.server_widget.host.text() == 'localhost' - assert window.server_widget.port.text() == '5555' + assert window.server_widget.host.text() == "localhost" + assert window.server_widget.port.text() == "5555" finally: window.close() station.disconnect() @@ -115,8 +123,8 @@ def test_client_station_gui_station_list_populated(qtbot, start_server): from instrumentserver.client.application import ClientStationGui from instrumentserver import QtCore - station = ClientStation(host='localhost', port=5555) - station.find_or_create_instrument('gui_cs_dummy', DUMMY_CLASS) + station = ClientStation(host="localhost", port=5555) + station.find_or_create_instrument("gui_cs_dummy", DUMMY_CLASS) window = ClientStationGui(station) qtbot.addWidget(window) try: @@ -131,19 +139,19 @@ def test_client_station_gui_open_instrument_tab(qtbot, start_server): from instrumentserver.gui.instruments import GenericInstrument from instrumentserver import QtCore - station = ClientStation(host='localhost', port=5555) - station.find_or_create_instrument('gui_cs_dummy2', DUMMY_CLASS) + station = ClientStation(host="localhost", port=5555) + station.find_or_create_instrument("gui_cs_dummy2", DUMMY_CLASS) window = ClientStationGui(station) qtbot.addWidget(window) try: items = window.stationList.findItems( - 'gui_cs_dummy2', QtCore.Qt.MatchExactly | QtCore.Qt.MatchRecursive, 0 + "gui_cs_dummy2", QtCore.Qt.MatchExactly | QtCore.Qt.MatchRecursive, 0 ) assert len(items) > 0 window.openInstrumentTab(items[0], 0) - assert 'gui_cs_dummy2' in window.instrumentTabsOpen - assert isinstance(window.instrumentTabsOpen['gui_cs_dummy2'], GenericInstrument) + assert "gui_cs_dummy2" in window.instrumentTabsOpen + assert isinstance(window.instrumentTabsOpen["gui_cs_dummy2"], GenericInstrument) finally: window.close() station.disconnect() diff --git a/test/pytest/test_config.py b/test/pytest/test_config.py index 09e1d45..82d63e0 100644 --- a/test/pytest/test_config.py +++ b/test/pytest/test_config.py @@ -1,4 +1,5 @@ """Tests for instrumentserver.config.loadConfig.""" + import pytest from pathlib import Path @@ -15,17 +16,23 @@ def _write_config(tmp_path: Path, content: str) -> Path: # Basic parsing # --------------------------------------------------------------------------- + def test_minimal_config(tmp_path): - cfg = _write_config(tmp_path, """\ + cfg = _write_config( + tmp_path, + """\ instruments: my_ins: type: instrumentserver.testing.dummy_instruments.generic.DummyInstrumentWithSubmodule -""") - path, serverConfig, fullConfig, tempFile, pollingRates, ipAddresses = loadConfig(cfg) +""", + ) + path, serverConfig, fullConfig, tempFile, pollingRates, ipAddresses = loadConfig( + cfg + ) tempFile.close() - assert 'my_ins' in serverConfig - assert 'my_ins' in fullConfig + assert "my_ins" in serverConfig + assert "my_ins" in fullConfig assert pollingRates == {} assert ipAddresses == {} # returned path is a string @@ -33,11 +40,14 @@ def test_minimal_config(tmp_path): def test_temp_file_is_readable(tmp_path): - cfg = _write_config(tmp_path, """\ + cfg = _write_config( + tmp_path, + """\ instruments: my_ins: type: some.Type -""") +""", + ) tempFilePath, _, _, tempFile, _, _ = loadConfig(cfg) tempFile.seek(0) content = tempFile.read() @@ -49,36 +59,46 @@ def test_temp_file_is_readable(tmp_path): # SERVERFIELDS defaults # --------------------------------------------------------------------------- + def test_initialize_defaults_to_true(tmp_path): - cfg = _write_config(tmp_path, """\ + cfg = _write_config( + tmp_path, + """\ instruments: my_ins: type: some.Type -""") +""", + ) _, serverConfig, _, tempFile, _, _ = loadConfig(cfg) tempFile.close() - assert serverConfig['my_ins']['initialize'] is True + assert serverConfig["my_ins"]["initialize"] is True def test_initialize_explicit_false(tmp_path): - cfg = _write_config(tmp_path, """\ + cfg = _write_config( + tmp_path, + """\ instruments: my_ins: type: some.Type initialize: false -""") +""", + ) _, serverConfig, _, tempFile, _, _ = loadConfig(cfg) tempFile.close() - assert serverConfig['my_ins']['initialize'] is False + assert serverConfig["my_ins"]["initialize"] is False def test_initialize_null_raises(tmp_path): - cfg = _write_config(tmp_path, """\ + cfg = _write_config( + tmp_path, + """\ instruments: my_ins: type: some.Type initialize: -""") +""", + ) with pytest.raises(AttributeError): loadConfig(cfg) @@ -87,37 +107,47 @@ def test_initialize_null_raises(tmp_path): # GUI field defaults # --------------------------------------------------------------------------- + def test_gui_defaults_to_generic_instrument(tmp_path): - cfg = _write_config(tmp_path, """\ + cfg = _write_config( + tmp_path, + """\ instruments: my_ins: type: some.Type -""") +""", + ) _, _, fullConfig, tempFile, _, _ = loadConfig(cfg) tempFile.close() - assert fullConfig['my_ins']['gui']['type'] == GUIFIELD['type'] + assert fullConfig["my_ins"]["gui"]["type"] == GUIFIELD["type"] def test_gui_generic_alias_maps_to_full_path(tmp_path): - cfg = _write_config(tmp_path, """\ + cfg = _write_config( + tmp_path, + """\ instruments: my_ins: type: some.Type gui: type: generic -""") +""", + ) _, _, fullConfig, tempFile, _, _ = loadConfig(cfg) tempFile.close() - assert fullConfig['my_ins']['gui']['type'] == GUIFIELD['type'] + assert fullConfig["my_ins"]["gui"]["type"] == GUIFIELD["type"] def test_gui_null_raises(tmp_path): - cfg = _write_config(tmp_path, """\ + cfg = _write_config( + tmp_path, + """\ instruments: my_ins: type: some.Type gui: -""") +""", + ) with pytest.raises(AttributeError): loadConfig(cfg) @@ -126,11 +156,15 @@ def test_gui_null_raises(tmp_path): # Error: missing instruments key # --------------------------------------------------------------------------- + def test_missing_instruments_key_raises(tmp_path): - cfg = _write_config(tmp_path, """\ + cfg = _write_config( + tmp_path, + """\ my_ins: type: some.Type -""") +""", + ) with pytest.raises(AttributeError): loadConfig(cfg) @@ -139,27 +173,34 @@ def test_missing_instruments_key_raises(tmp_path): # pollingRate # --------------------------------------------------------------------------- + def test_polling_rate_parsed(tmp_path): - cfg = _write_config(tmp_path, """\ + cfg = _write_config( + tmp_path, + """\ instruments: my_ins: type: some.Type pollingRate: param1: 100 param2: 200 -""") +""", + ) _, _, _, tempFile, pollingRates, _ = loadConfig(cfg) tempFile.close() - assert pollingRates == {'my_ins.param1': 100, 'my_ins.param2': 200} + assert pollingRates == {"my_ins.param1": 100, "my_ins.param2": 200} def test_polling_rate_empty_is_ignored(tmp_path): - cfg = _write_config(tmp_path, """\ + cfg = _write_config( + tmp_path, + """\ instruments: my_ins: type: some.Type pollingRate: -""") +""", + ) _, _, _, tempFile, pollingRates, _ = loadConfig(cfg) tempFile.close() assert pollingRates == {} @@ -169,27 +210,34 @@ def test_polling_rate_empty_is_ignored(tmp_path): # networking # --------------------------------------------------------------------------- + def test_networking_parsed(tmp_path): - cfg = _write_config(tmp_path, """\ + cfg = _write_config( + tmp_path, + """\ instruments: my_ins: type: some.Type networking: externalBroadcast: tcp://192.168.1.1:5556 listeningAddress: 192.168.1.1 -""") +""", + ) _, _, _, tempFile, _, ipAddresses = loadConfig(cfg) tempFile.close() - assert ipAddresses['externalBroadcast'] == 'tcp://192.168.1.1:5556' - assert ipAddresses['listeningAddress'] == '192.168.1.1' + assert ipAddresses["externalBroadcast"] == "tcp://192.168.1.1:5556" + assert ipAddresses["listeningAddress"] == "192.168.1.1" def test_no_networking_section_gives_empty_dict(tmp_path): - cfg = _write_config(tmp_path, """\ + cfg = _write_config( + tmp_path, + """\ instruments: my_ins: type: some.Type -""") +""", + ) _, _, _, tempFile, _, ipAddresses = loadConfig(cfg) tempFile.close() assert ipAddresses == {} @@ -199,8 +247,11 @@ def test_no_networking_section_gives_empty_dict(tmp_path): # gui_defaults merging # --------------------------------------------------------------------------- + def test_gui_defaults_default_section(tmp_path): - cfg = _write_config(tmp_path, """\ + cfg = _write_config( + tmp_path, + """\ instruments: my_ins: type: some.module.MyClass @@ -208,16 +259,19 @@ def test_gui_defaults_default_section(tmp_path): __default__: parameters-hide: - IDN -""") +""", + ) _, _, fullConfig, tempFile, _, _ = loadConfig(cfg) tempFile.close() - kwargs = fullConfig['my_ins']['gui'].get('kwargs', {}) - assert 'parameters-hide' in kwargs - assert 'IDN' in kwargs['parameters-hide'] + kwargs = fullConfig["my_ins"]["gui"].get("kwargs", {}) + assert "parameters-hide" in kwargs + assert "IDN" in kwargs["parameters-hide"] def test_gui_defaults_class_section(tmp_path): - cfg = _write_config(tmp_path, """\ + cfg = _write_config( + tmp_path, + """\ instruments: my_ins: type: some.module.MyClass @@ -225,17 +279,20 @@ def test_gui_defaults_class_section(tmp_path): MyClass: parameters-hide: - power_level -""") +""", + ) _, _, fullConfig, tempFile, _, _ = loadConfig(cfg) tempFile.close() - kwargs = fullConfig['my_ins']['gui'].get('kwargs', {}) - assert 'parameters-hide' in kwargs - assert 'power_level' in kwargs['parameters-hide'] + kwargs = fullConfig["my_ins"]["gui"].get("kwargs", {}) + assert "parameters-hide" in kwargs + assert "power_level" in kwargs["parameters-hide"] def test_gui_defaults_merging_order(tmp_path): """__default__ + class + instance patterns all appear in merged result.""" - cfg = _write_config(tmp_path, """\ + cfg = _write_config( + tmp_path, + """\ instruments: my_ins: type: some.module.MyClass @@ -250,10 +307,11 @@ def test_gui_defaults_merging_order(tmp_path): MyClass: parameters-hide: - class_param -""") +""", + ) _, _, fullConfig, tempFile, _, _ = loadConfig(cfg) tempFile.close() - hide = fullConfig['my_ins']['gui']['kwargs']['parameters-hide'] - assert 'default_param' in hide - assert 'class_param' in hide - assert 'instance_param' in hide \ No newline at end of file + hide = fullConfig["my_ins"]["gui"]["kwargs"]["parameters-hide"] + assert "default_param" in hide + assert "class_param" in hide + assert "instance_param" in hide diff --git a/test/pytest/test_helpers.py b/test/pytest/test_helpers.py index 77d92fd..c284210 100644 --- a/test/pytest/test_helpers.py +++ b/test/pytest/test_helpers.py @@ -15,6 +15,7 @@ # stringToArgsAndKwargs # --------------------------------------------------------------------------- + def test_stringToArgsAndKwargs_empty_string(): args, kwargs = stringToArgsAndKwargs("") assert args == [] @@ -27,13 +28,16 @@ def test_stringToArgsAndKwargs_whitespace_only(): assert kwargs == {} -@pytest.mark.parametrize("value, expected_args, expected_kwargs", [ - ("1, True", [1, True], {}), - ("'hello'", ['hello'], {}), - ("1, 2, 3", [1, 2, 3], {}), - ("x=1, y=2", [], {'x': 1, 'y': 2}), - ("1, abc=12.3", [1], {'abc': 12.3}), -]) +@pytest.mark.parametrize( + "value, expected_args, expected_kwargs", + [ + ("1, True", [1, True], {}), + ("'hello'", ["hello"], {}), + ("1, 2, 3", [1, 2, 3], {}), + ("x=1, y=2", [], {"x": 1, "y": 2}), + ("1, abc=12.3", [1], {"abc": 12.3}), + ], +) def test_stringToArgsAndKwargs_valid(value, expected_args, expected_kwargs): args, kwargs = stringToArgsAndKwargs(value) assert args == expected_args @@ -59,6 +63,7 @@ def test_stringToArgsAndKwargs_unevaluable_kwarg_value(): # flat_to_nested_dict # --------------------------------------------------------------------------- + def test_flat_to_nested_dict_already_flat(): flat = {"a": 1, "b": 2} result = flat_to_nested_dict(flat) @@ -85,6 +90,7 @@ def test_flat_to_nested_dict_empty(): # flatten_dict # --------------------------------------------------------------------------- + def test_flatten_dict_already_flat(): d = {"a": 1, "b": 2} result = flatten_dict(d) @@ -99,7 +105,7 @@ def test_flatten_dict_nested(): def test_flatten_dict_custom_sep(): nested = {"a": {"b": 1}} - result = flatten_dict(nested, sep='/') + result = flatten_dict(nested, sep="/") assert result == {"a/b": 1} @@ -114,6 +120,7 @@ def test_flatten_dict_round_trip(): # is_flat_dict # --------------------------------------------------------------------------- + def test_is_flat_dict_flat(): assert is_flat_dict({"a": 1, "b": "hello"}) is True @@ -134,6 +141,7 @@ def test_is_flat_dict_empty(): # nestedAttributeFromString # --------------------------------------------------------------------------- + class _Root: class _Child: value = 42 @@ -143,58 +151,59 @@ class _Child: def test_nestedAttributeFromString_single_level(): root = _Root() - assert nestedAttributeFromString(root, 'scalar') == 99 + assert nestedAttributeFromString(root, "scalar") == 99 def test_nestedAttributeFromString_two_levels(): root = _Root() - assert nestedAttributeFromString(root, '_Child.value') == 42 + assert nestedAttributeFromString(root, "_Child.value") == 42 def test_nestedAttributeFromString_missing_raises(): root = _Root() with pytest.raises(AttributeError): - nestedAttributeFromString(root, 'nonexistent_attr') + nestedAttributeFromString(root, "nonexistent_attr") def test_nestedAttributeFromString_nested_missing_raises(): root = _Root() with pytest.raises(AttributeError): - nestedAttributeFromString(root, '_Child.nonexistent') + nestedAttributeFromString(root, "_Child.nonexistent") # --------------------------------------------------------------------------- # typeClassPath / objectClassPath # --------------------------------------------------------------------------- + class _MyClass: pass def test_typeClassPath_contains_class_name(): path = typeClassPath(_MyClass) - assert '_MyClass' in path - assert '.' in path + assert "_MyClass" in path + assert "." in path def test_objectClassPath_contains_class_name(): obj = _MyClass() path = objectClassPath(obj) - assert '_MyClass' in path - assert '.' in path + assert "_MyClass" in path + assert "." in path def test_typeClassPath_builtin(): path = typeClassPath(int) - assert 'int' in path + assert "int" in path def test_objectClassPath_builtin_instance(): path = objectClassPath(42) - assert 'int' in path + assert "int" in path def test_typeClassPath_and_objectClassPath_agree(): """typeClassPath on the class and objectClassPath on an instance should match.""" obj = _MyClass() - assert typeClassPath(_MyClass) == objectClassPath(obj) \ No newline at end of file + assert typeClassPath(_MyClass) == objectClassPath(obj) diff --git a/test/pytest/test_json_serializable.py b/test/pytest/test_json_serializable.py index 011e782..36ff5bf 100644 --- a/test/pytest/test_json_serializable.py +++ b/test/pytest/test_json_serializable.py @@ -2,21 +2,24 @@ import qcodes as qc from qcodes.math_utils.field_vector import FieldVector -from instrumentserver.blueprints import (bluePrintFromInstrumentModule, - bluePrintFromMethod, - bluePrintFromParameter, - bluePrintToDict, - deserialize_obj, - ParameterBroadcastBluePrint, - iterable_to_serialized_dict, - dict_to_serialized_dict, to_dict, - ) -from instrumentserver.testing.dummy_instruments.generic import DummyInstrumentWithSubmodule +from instrumentserver.blueprints import ( + bluePrintFromInstrumentModule, + bluePrintFromMethod, + bluePrintFromParameter, + bluePrintToDict, + deserialize_obj, + ParameterBroadcastBluePrint, + iterable_to_serialized_dict, + dict_to_serialized_dict, + to_dict, +) +from instrumentserver.testing.dummy_instruments.generic import ( + DummyInstrumentWithSubmodule, +) from instrumentserver.testing.dummy_instruments.rf import ResonatorResponse class CustomParameter(qc.Parameter): - def __int__(self, name, *args, **kwargs): """ Well lets see if you go anywhere @@ -33,23 +36,21 @@ def set_raw(self, val): class MyClass: - - attributes = ['x', 'y', 'z'] + attributes = ["x", "y", "z"] def __init__(self, x=1, y=2, z=3): self.x = x self.y = y self.z = z - - def customFunction(self, x: int, y:int) -> int: - print(f'I am in my function') - return x*y + def customFunction(self, x: int, y: int) -> int: + print(f"I am in my function") + return x * y def test_basic_param_dictionary(): - my_param = CustomParameter(name='my_param', unit='M') - param_bp = bluePrintFromParameter('', my_param) + my_param = CustomParameter(name="my_param", unit="M") + param_bp = bluePrintFromParameter("", my_param) bp_dict = bluePrintToDict(param_bp) reconstructed_bp = deserialize_obj(bp_dict) assert param_bp == reconstructed_bp @@ -64,13 +65,13 @@ def test_basic_function_dictionary(): def test_basic_instrument_dictionary(): - my_rr = ResonatorResponse('rr') + my_rr = ResonatorResponse("rr") instrument_bp = bluePrintFromInstrumentModule("", my_rr) bp_dict = bluePrintToDict(instrument_bp) reconstructed_bp = deserialize_obj(bp_dict) assert instrument_bp == reconstructed_bp - my_dummy = DummyInstrumentWithSubmodule('dummy') + my_dummy = DummyInstrumentWithSubmodule("dummy") dummy_bp = bluePrintFromInstrumentModule("", my_dummy) dummy_bp_dict = bluePrintToDict(dummy_bp) reconstructed_dummy_bp = deserialize_obj(dummy_bp_dict) @@ -78,7 +79,9 @@ def test_basic_instrument_dictionary(): def test_basic_broadcast_parameter_dictionary(): - broadcast_bp = ParameterBroadcastBluePrint(name='my_param', action='an_action', value=-56, unit='M') + broadcast_bp = ParameterBroadcastBluePrint( + name="my_param", action="an_action", value=-56, unit="M" + ) bp_dict = bluePrintToDict(broadcast_bp) reconstructed_bp = deserialize_obj(bp_dict) assert broadcast_bp == reconstructed_bp @@ -88,20 +91,35 @@ def test_arbitrary_class_serialization(): arbitrary_class_1 = MyClass() arbitrary_class_2 = MyClass(x=10, y=11, z=12) - expected_arg = [{'x': 1, 'y': 2, 'z': 3, - '_class_type': f'{arbitrary_class_1.__module__}.{arbitrary_class_1.__class__.__name__}'}] - expected_kwargs = {'arbitrary_class_2': {'x': 10, 'y': 11, 'z': 12, - '_class_type': f'{arbitrary_class_2.__module__}.{arbitrary_class_2.__class__.__name__}'}} + expected_arg = [ + { + "x": 1, + "y": 2, + "z": 3, + "_class_type": f"{arbitrary_class_1.__module__}.{arbitrary_class_1.__class__.__name__}", + } + ] + expected_kwargs = { + "arbitrary_class_2": { + "x": 10, + "y": 11, + "z": 12, + "_class_type": f"{arbitrary_class_2.__module__}.{arbitrary_class_2.__class__.__name__}", + } + } returned_args = iterable_to_serialized_dict([arbitrary_class_1]) - returned_kwargs = dict_to_serialized_dict({'arbitrary_class_2': arbitrary_class_2}) + returned_kwargs = dict_to_serialized_dict({"arbitrary_class_2": arbitrary_class_2}) assert returned_args == expected_arg assert expected_kwargs == returned_kwargs def test_send_arbitrary_objects(cli): - field_vector_ins = cli.find_or_create_instrument('field_vector', instrument_class="instrumentserver.testing.dummy_instruments.generic.FieldVectorIns") + field_vector_ins = cli.find_or_create_instrument( + "field_vector", + instrument_class="instrumentserver.testing.dummy_instruments.generic.FieldVectorIns", + ) new_vector = FieldVector(x=12.0, y=12.0, z=12.0) field_vector_ins.set_field(new_vector) @@ -111,8 +129,10 @@ def test_send_arbitrary_objects(cli): def test_sending_complex_numbers(cli): - field_vector_ins = cli.find_or_create_instrument('field_vector', - instrument_class="instrumentserver.testing.dummy_instruments.generic.FieldVectorIns") + field_vector_ins = cli.find_or_create_instrument( + "field_vector", + instrument_class="instrumentserver.testing.dummy_instruments.generic.FieldVectorIns", + ) # Getting value expected_complex = 1 + 1j diff --git a/test/pytest/test_param_manager.py b/test/pytest/test_param_manager.py index e102607..4fc4549 100644 --- a/test/pytest/test_param_manager.py +++ b/test/pytest/test_param_manager.py @@ -5,39 +5,47 @@ def prep_param_manager(params, template=1): if template == 1: - params.add_parameter(name='my_param', initial_value=123, unit='M') - params.add_parameter(name='nested_param.child1', initial_value=456, unit='a') - params.add_parameter(name='nested_param.child2', initial_value=789, unit='b') - params.add_parameter(name='nested_param.how.are.you', initial_value=111, unit='b') - params.add_parameter(name='nested_param.how.are.too', initial_value=222, unit='b') + params.add_parameter(name="my_param", initial_value=123, unit="M") + params.add_parameter(name="nested_param.child1", initial_value=456, unit="a") + params.add_parameter(name="nested_param.child2", initial_value=789, unit="b") + params.add_parameter( + name="nested_param.how.are.you", initial_value=111, unit="b" + ) + params.add_parameter( + name="nested_param.how.are.too", initial_value=222, unit="b" + ) elif template == 2: - params.add_parameter(name='his_param', initial_value=-123, unit='n') - params.add_parameter(name='nested_param.son1', initial_value=-456, unit='c') - params.add_parameter(name='nested_param.son2', initial_value=-789, unit='d') + params.add_parameter(name="his_param", initial_value=-123, unit="n") + params.add_parameter(name="nested_param.son1", initial_value=-456, unit="c") + params.add_parameter(name="nested_param.son2", initial_value=-789, unit="d") elif template == 3: - params.add_parameter(name='her_param', initial_value=0, unit='p') - params.add_parameter(name='nested_param.daughter1', initial_value=1, unit='e') - params.add_parameter(name='nested_param.daughter2', initial_value=2, unit='f') + params.add_parameter(name="her_param", initial_value=0, unit="p") + params.add_parameter(name="nested_param.daughter1", initial_value=1, unit="e") + params.add_parameter(name="nested_param.daughter2", initial_value=2, unit="f") elif template == 4: - params.add_parameter(name='our_param', initial_value=3, unit='m') - params.add_parameter(name='nested_param.sibling1', initial_value=[4, 6], unit='g') + params.add_parameter(name="our_param", initial_value=3, unit="m") + params.add_parameter( + name="nested_param.sibling1", initial_value=[4, 6], unit="g" + ) params.nested_param.sibling1(1234856834) - params.add_parameter(name='nested_param.sibling2', initial_value=[5, 7], unit='h') + params.add_parameter( + name="nested_param.sibling2", initial_value=[5, 7], unit="h" + ) def test_param(param_manager): cli, params = param_manager - params.add_parameter(name='my_param', initial_value=123, unit='M') + params.add_parameter(name="my_param", initial_value=123, unit="M") assert params.my_param() == 123 - assert params.my_param.unit == 'M' + assert params.my_param.unit == "M" params.my_param(456) assert params.my_param() == 456 def test_removing_all_params(): - params = ParameterManager(name='params') + params = ParameterManager(name="params") prep_param_manager(params) prep_param_manager(params, template=2) @@ -50,37 +58,41 @@ def test_removing_all_params(): def test_finding_all_profiles(tmp_path): - params = ParameterManager(name='params') + params = ParameterManager(name="params") prep_param_manager(params) - params.toFile(tmp_path, 'first') + params.toFile(tmp_path, "first") prep_param_manager(params, template=2) - params.toFile(tmp_path, 'second') + params.toFile(tmp_path, "second") prep_param_manager(params, template=3) - params.toFile(tmp_path, 'third') + params.toFile(tmp_path, "third") prep_param_manager(params, template=4) - params.toFile(tmp_path, 'fourth') + params.toFile(tmp_path, "fourth") params.workingDirectory = tmp_path profiles = params.refresh_profiles() - assert sorted(profiles) == sorted(['parameter_manager-first.json', - 'parameter_manager-second.json', - 'parameter_manager-third.json', - 'parameter_manager-fourth.json']) + assert sorted(profiles) == sorted( + [ + "parameter_manager-first.json", + "parameter_manager-second.json", + "parameter_manager-third.json", + "parameter_manager-fourth.json", + ] + ) def test_saving_correct_profile(tmp_path): - params = ParameterManager(name='params') + params = ParameterManager(name="params") params.workingDirectory = tmp_path prep_param_manager(params) - params.toFile(name='first') - file_path = tmp_path.joinpath('parameter_manager-first.json') + params.toFile(name="first") + file_path = tmp_path.joinpath("parameter_manager-first.json") assert file_path.exists() params.my_param(8888) @@ -89,24 +101,24 @@ def test_saving_correct_profile(tmp_path): with open(file_path) as file: data = json.load(file) - assert data['params.my_param']['value'] == 8888 + assert data["params.my_param"]["value"] == 8888 def test_loading_correct_profile(tmp_path): - params = ParameterManager(name='params') + params = ParameterManager(name="params") params.workingDirectory = tmp_path prep_param_manager(params) - params.toFile(name='first') - file_path = tmp_path.joinpath('parameter_manager-first.json') + params.toFile(name="first") + file_path = tmp_path.joinpath("parameter_manager-first.json") with open(file_path) as file: data = json.load(file) - data['params.my_param']['value'] = 9999 + data["params.my_param"]["value"] = 9999 - with open(file_path, 'w') as file: + with open(file_path, "w") as file: json.dump(data, file) params.fromFile() @@ -114,15 +126,15 @@ def test_loading_correct_profile(tmp_path): def prep_switching_profiles(tmp_path): - params = ParameterManager(name='params') + params = ParameterManager(name="params") prep_param_manager(params) - params.toFile(tmp_path, 'first') + params.toFile(tmp_path, "first") params.remove_all_parameters() prep_param_manager(params, template=2) - params.toFile(tmp_path, 'second') + params.toFile(tmp_path, "second") params.workingDirectory = tmp_path params.refresh_profiles() @@ -134,7 +146,7 @@ def test_switching_profiles(tmp_path): params = prep_switching_profiles(tmp_path) - params.switch_to_profile('parameter_manager-first.json') + params.switch_to_profile("parameter_manager-first.json") assert params.my_param() == 123 assert params.nested_param.child1() == 456 @@ -144,13 +156,13 @@ def test_switching_profiles(tmp_path): def test_switching_profiles_short_name(tmp_path): params = prep_switching_profiles(tmp_path) - params.switch_to_profile('first') + params.switch_to_profile("first") assert params.my_param() == 123 assert params.nested_param.child1() == 456 assert params.nested_param.child2() == 789 - params.switch_to_profile('second') + params.switch_to_profile("second") assert params.his_param() == -123 assert params.nested_param.son1() == -456 @@ -164,32 +176,31 @@ def test_switching_profiles_automatic_save(tmp_path): params.nested_param.son1(222) params.nested_param.son2(333) - params.switch_to_profile('first') + params.switch_to_profile("first") - with open(tmp_path.joinpath('parameter_manager-second.json')) as file: + with open(tmp_path.joinpath("parameter_manager-second.json")) as file: second = json.load(file) - assert second['params.his_param']['value'] == 111 - assert second['params.nested_param.son1']['value'] == 222 - assert second['params.nested_param.son2']['value'] == 333 + assert second["params.his_param"]["value"] == 111 + assert second["params.nested_param.son1"]["value"] == 222 + assert second["params.nested_param.son2"]["value"] == 333 def test_selectedProfile_only_changing_when_correct_name(tmp_path): - params = ParameterManager(name='params') + params = ParameterManager(name="params") prep_param_manager(params) - names_path = tmp_path.joinpath('names.json') + names_path = tmp_path.joinpath("names.json") params.toFile(names_path) - assert params.selectedProfile == 'parameter_manager-params.json' + assert params.selectedProfile == "parameter_manager-params.json" params.fromFile(names_path) - assert params.selectedProfile == 'parameter_manager-params.json' + assert params.selectedProfile == "parameter_manager-params.json" - params.toFile(tmp_path, 'params') - assert params.selectedProfile == 'parameter_manager-params.json' + params.toFile(tmp_path, "params") + assert params.selectedProfile == "parameter_manager-params.json" - new_path = names_path.replace(tmp_path.joinpath('parameter_manager-names.json')) + new_path = names_path.replace(tmp_path.joinpath("parameter_manager-names.json")) params.fromFile(new_path) - assert params.selectedProfile == 'parameter_manager-names.json' - + assert params.selectedProfile == "parameter_manager-names.json" diff --git a/test/pytest/test_serialize.py b/test/pytest/test_serialize.py index 9ab5e5e..b8764ef 100644 --- a/test/pytest/test_serialize.py +++ b/test/pytest/test_serialize.py @@ -6,7 +6,7 @@ def test_toParamDict_paramsBasic(): """Test serializing a few parameters added to a station""" paramNames = [f"parameter_{i}" for i in range(4)] - paramValues = [123, None, True, 'abcdef'] + paramValues = [123, None, True, "abcdef"] params = [] for n, v in zip(paramNames, paramValues): @@ -21,4 +21,3 @@ def test_toParamDict_paramsBasic(): paramDict_expt[n] = v assert paramDict_test == paramDict_expt - diff --git a/test/pytest/test_server_gui.py b/test/pytest/test_server_gui.py index 106c847..daf0e58 100644 --- a/test/pytest/test_server_gui.py +++ b/test/pytest/test_server_gui.py @@ -14,7 +14,7 @@ def _shutdown_server_window(window): window.close() except Exception: pass - thread = getattr(window, 'stationServerThread', None) + thread = getattr(window, "stationServerThread", None) if thread is not None: try: thread.wait(5000) @@ -34,13 +34,13 @@ def test_saving_button(qtbot): "rr.resonator_frequency": 5000000000.0, "rr.resonator_linewidth": 1000000.0, "rr.start_frequency": 20000.0, - "rr.stop_frequency": 20000000000.0 + "rr.stop_frequency": 20000000000.0, } - window = startServerGuiApplication() - rr = window.client.find_or_create_instrument('rr', - 'instrumentserver.testing.dummy_instruments.rf.ResonatorResponse') + rr = window.client.find_or_create_instrument( + "rr", "instrumentserver.testing.dummy_instruments.rf.ResonatorResponse" + ) qtbot.addWidget(window) saving_widget = window.toolBar.widgetForAction(window.saveParamsAction) @@ -49,7 +49,7 @@ def test_saving_button(qtbot): file_path = Path(window._paramValuesFile) try: assert file_path.is_file() - with open(str(file_path), 'r') as f: + with open(str(file_path), "r") as f: loaded_file = json.load(f) assert correct_file_dict == flatten_dict(loaded_file) @@ -74,11 +74,13 @@ def test_loading_button(qtbot): file_path = Path(window._paramValuesFile) - with open(str(file_path), 'w+') as f: + with open(str(file_path), "w+") as f: json.dump(correct_file_dict, f) - dummy = window.client.find_or_create_instrument('dummy', - 'instrumentserver.testing.dummy_instruments.generic.DummyInstrumentWithSubmodule') + dummy = window.client.find_or_create_instrument( + "dummy", + "instrumentserver.testing.dummy_instruments.generic.DummyInstrumentWithSubmodule", + ) qtbot.addWidget(window) loading_widget = window.toolBar.widgetForAction(window.loadParamsAction) @@ -98,8 +100,10 @@ def test_refresh_button(qtbot): try: assert window.stationList.topLevelItemCount() == 0 - dummy = window.client.find_or_create_instrument('dummy', - 'instrumentserver.testing.dummy_instruments.generic.DummyInstrumentWithSubmodule') + dummy = window.client.find_or_create_instrument( + "dummy", + "instrumentserver.testing.dummy_instruments.generic.DummyInstrumentWithSubmodule", + ) refresh_widget = window.toolBar.widgetForAction(window.refreshStationAction) qtbot.mouseClick(refresh_widget, QtCore.Qt.LeftButton) @@ -117,11 +121,15 @@ def test_clicking_an_item(qtbot): try: assert window.stationList.topLevelItemCount() == 0 - dummy = window.client.find_or_create_instrument('dummy', - 'instrumentserver.testing.dummy_instruments.generic.DummyInstrumentWithSubmodule') + dummy = window.client.find_or_create_instrument( + "dummy", + "instrumentserver.testing.dummy_instruments.generic.DummyInstrumentWithSubmodule", + ) window.refreshStationAction.trigger() - item = window.stationList.findItems('dummy', QtCore.Qt.MatchExactly | QtCore.Qt.MatchRecursive, 0) + item = window.stationList.findItems( + "dummy", QtCore.Qt.MatchExactly | QtCore.Qt.MatchRecursive, 0 + ) widget = item[0].treeWidget() qtbot.mouseClick(widget, QtCore.Qt.LeftButton) @@ -134,17 +142,21 @@ def test_opening_new_tab_generic_object(qtbot): window = startServerGuiApplication() qtbot.addWidget(window) try: - dummy = window.client.find_or_create_instrument('dummy', - 'instrumentserver.testing.dummy_instruments.generic.DummyInstrumentWithSubmodule') + dummy = window.client.find_or_create_instrument( + "dummy", + "instrumentserver.testing.dummy_instruments.generic.DummyInstrumentWithSubmodule", + ) window.refreshStationAction.trigger() - item = window.stationList.findItems('dummy', QtCore.Qt.MatchExactly | QtCore.Qt.MatchRecursive, 0) + item = window.stationList.findItems( + "dummy", QtCore.Qt.MatchExactly | QtCore.Qt.MatchRecursive, 0 + ) # Manually triggering the tab opening since qtbot refuses to double-click an item window.addInstrumentTab(item[0], 0) - assert 'dummy' in window.instrumentTabsOpen + assert "dummy" in window.instrumentTabsOpen - assert isinstance(window.instrumentTabsOpen['dummy'], GenericInstrument) + assert isinstance(window.instrumentTabsOpen["dummy"], GenericInstrument) finally: _shutdown_server_window(window) diff --git a/test/test_async_requests/client_station_gui.py b/test/test_async_requests/client_station_gui.py index 013d9c5..314d48b 100644 --- a/test/test_async_requests/client_station_gui.py +++ b/test/test_async_requests/client_station_gui.py @@ -3,7 +3,8 @@ from instrumentserver.client import ClientStation from instrumentserver.client.application import ClientStationGui -from instrumentserver import QtWidgets +from instrumentserver import QtWidgets + if __name__ == "__main__": # gather some test config files @@ -11,16 +12,16 @@ # create client station app = QtWidgets.QApplication(sys.argv) - cli_station = ClientStation(host="localhost", config_path=config_path)#, param_path=param_path, port=5555) - + cli_station = ClientStation( + host="localhost", config_path=config_path + ) # , param_path=param_path, port=5555) + # test client station functions print(cli_station.get_parameters()["test1"]["param1"]) test1 = cli_station["test1"] print(test1.get_random()) - + # make and display gui window win = ClientStationGui(cli_station) win.show() sys.exit(app.exec_()) - - diff --git a/test/test_async_requests/demo_concurrency.py b/test/test_async_requests/demo_concurrency.py index d8a1d2b..ff626fb 100644 --- a/test/test_async_requests/demo_concurrency.py +++ b/test/test_async_requests/demo_concurrency.py @@ -2,7 +2,7 @@ import sys import time -''' +""" Simple concurrency demo. Usage (server already running): @@ -23,7 +23,7 @@ This mimics the case when one client is ramping bias voltage, while another client wants to change a parameter of a different instrument. Or more commonly, a client is ramping bias voltage, and we want to view parameter of an instrument in the server gui (which also is basically another client that runs in a different thread.) -''' +""" if __name__ == "__main__": role = sys.argv[1] if len(sys.argv) > 1 else "ramp" @@ -43,17 +43,21 @@ t0 = time.time() - if role == "ramp": # within a single process, operations are always blocking + if role == "ramp": # within a single process, operations are always blocking print("[ramp] dummy1.get_random_timeout(10)") print(dummy1.get_random_timeout(10)) print("[after ramp] dummy2.get_random()") print(dummy2.get_random()) - elif role == "same": # from a different process, operations on the same instrument are still blocked + elif ( + role == "same" + ): # from a different process, operations on the same instrument are still blocked print("[same] dummy1.get_random() (same instrument as ramp)") print(dummy1.get_random()) - elif role == "other": # from a different process, operations on a different instrument are NOT blocked + elif ( + role == "other" + ): # from a different process, operations on a different instrument are NOT blocked print("[other] dummy2.get_random() (different instrument)") print(dummy2.get_random()) @@ -61,5 +65,3 @@ print(f"Unknown role {role!r}. Use 'ramp', 'same', or 'other'.") print(f"[{role}] took {time.time() - t0:.3f} s") - - diff --git a/test/test_async_requests/test_client.py b/test/test_async_requests/test_client.py index d41f2ac..309b5c4 100644 --- a/test/test_async_requests/test_client.py +++ b/test/test_async_requests/test_client.py @@ -1,28 +1,31 @@ from instrumentserver.client import Client -''' +""" A simple script for testing the new features on the server/client. -''' +""" if __name__ == "__main__": cli = Client(timeout=15000, port=5555) import time + t0 = time.time() - dummy1 = cli.find_or_create_instrument('test1', - 'instrumentserver.testing.dummy_instruments.generic.DummyInstrumentTimeout') - dummy2 = cli.find_or_create_instrument('test2', - 'instrumentserver.testing.dummy_instruments.generic.DummyInstrumentTimeout') - + dummy1 = cli.find_or_create_instrument( + "test1", + "instrumentserver.testing.dummy_instruments.generic.DummyInstrumentTimeout", + ) + dummy2 = cli.find_or_create_instrument( + "test2", + "instrumentserver.testing.dummy_instruments.generic.DummyInstrumentTimeout", + ) + # print(dummy1.get_random_timeout(10)) print(dummy2.get_random()) dummy1.param2(1e9) - + # for i in range(20): # print(dummy1.get_random()) # print(dummy2.get_random()) - - print(f"took {time.time() - t0} seconds") - + print(f"took {time.time() - t0} seconds") From b119d32911b4e53f272aa27f83ac1beafca4ff0c Mon Sep 17 00:00:00 2001 From: marcosf2 Date: Fri, 24 Apr 2026 12:35:31 -0300 Subject: [PATCH 06/12] Fix all linter errors --- pyproject.toml | 3 + src/instrumentserver/__init__.py | 14 ++--- src/instrumentserver/apps.py | 18 +++--- src/instrumentserver/base.py | 5 +- src/instrumentserver/blueprints.py | 16 +++--- src/instrumentserver/client/__init__.py | 8 ++- src/instrumentserver/client/application.py | 21 +++---- src/instrumentserver/client/core.py | 8 +-- src/instrumentserver/client/proxy.py | 26 ++++----- src/instrumentserver/config.py | 6 +- src/instrumentserver/gui/__init__.py | 2 +- src/instrumentserver/gui/base_instrument.py | 3 +- src/instrumentserver/gui/instruments.py | 31 +++++------ src/instrumentserver/gui/misc.py | 4 +- src/instrumentserver/gui/parameters.py | 14 ++--- src/instrumentserver/helpers.py | 3 +- src/instrumentserver/log.py | 6 +- src/instrumentserver/monitoring/listener.py | 6 +- src/instrumentserver/params.py | 11 ++-- src/instrumentserver/serialize.py | 11 ++-- src/instrumentserver/server/application.py | 20 +++---- src/instrumentserver/server/core.py | 55 ++++++++----------- src/instrumentserver/server/pollingWorker.py | 1 - .../testing/dummy_instruments/generic.py | 8 +-- .../testing/dummy_instruments/rf.py | 5 +- test/client_workingdir/init_client.py | 3 +- test/notebooks/Autoupdate.ipynb | 14 +---- .../Prototype the ParamManager.ipynb | 21 ++----- ...n the station server from a notebook.ipynb | 2 +- test/prototyping/server_admin.py | 9 +-- test/prototyping/testing_parameter_manager.py | 12 +--- test/pytest/conftest.py | 3 +- test/pytest/test_base.py | 11 ++-- test/pytest/test_basic_functionality.py | 4 +- test/pytest/test_client_station.py | 7 +-- test/pytest/test_config.py | 5 +- test/pytest/test_helpers.py | 5 +- test/pytest/test_json_serializable.py | 7 +-- test/pytest/test_param_manager.py | 11 ++++ test/pytest/test_serialize.py | 3 +- test/pytest/test_server_gui.py | 8 +-- .../test_async_requests/client_station_gui.py | 4 +- test/test_async_requests/demo_concurrency.py | 3 +- test/test_async_requests/test_client.py | 1 - 44 files changed, 195 insertions(+), 243 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 74e4945..730ab47 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,9 @@ exclude = ["docs"] [tool.ruff.lint] extend-select = ["I"] +[tool.ruff.lint.per-file-ignores] +"*.ipynb" = ["E402"] + [tool.mypy] files = ["src"] strict_optional = true diff --git a/src/instrumentserver/__init__.py b/src/instrumentserver/__init__.py index a41991c..c8b9129 100644 --- a/src/instrumentserver/__init__.py +++ b/src/instrumentserver/__init__.py @@ -1,9 +1,9 @@ -import sys -import os -import logging import json +import os -from qtpy import QtGui, QtCore, QtWidgets +from qtpy import QtCore as QtCore +from qtpy import QtGui as QtGui +from qtpy import QtWidgets as QtWidgets def getInstrumentserverPath(*subfolder: str) -> str: @@ -27,8 +27,8 @@ def getInstrumentserverPath(*subfolder: str) -> str: with open(PARAMS_SCHEMA_PATH) as f: paramDictSchema = json.load(f) -from .log import setupLogging, logger - -from .client import Client +from .client import Client # noqa: E402 +from .log import logger as logger # noqa: E402 +from .log import setupLogging as setupLogging # noqa: E402 InstrumentClient = Client diff --git a/src/instrumentserver/apps.py b/src/instrumentserver/apps.py index a830703..2a1c2ec 100644 --- a/src/instrumentserver/apps.py +++ b/src/instrumentserver/apps.py @@ -1,24 +1,22 @@ -import os import argparse import logging +import os import signal from pathlib import Path -from . import QtWidgets, QtCore -from .log import setupLogging -from .config import loadConfig -from .server.application import startServerGuiApplication -from .server.core import startServer +from instrumentserver.server.application import DetachedServerGui +from . import QtCore, QtWidgets from .client import Client, ClientStation from .client.application import ClientStationGui +from .config import loadConfig from .gui import widgetMainWindow from .gui.instruments import ParameterManagerGui +from .log import setupLogging +from .server.application import startServerGuiApplication +from .server.core import startServer from .server.pollingWorker import PollingWorker -from instrumentserver.server.application import DetachedServerGui - - setupLogging(addStreamHandler=True, logFile=os.path.abspath("instrumentserver.log")) logger = logging.getLogger("instrumentserver") logger.setLevel(logging.INFO) @@ -37,7 +35,7 @@ def server(**kwargs): def serverWithGui(**kwargs): app = QtWidgets.QApplication([]) - window = startServerGuiApplication(**kwargs) + startServerGuiApplication(**kwargs) return app.exec_() diff --git a/src/instrumentserver/base.py b/src/instrumentserver/base.py index 1204630..9987514 100644 --- a/src/instrumentserver/base.py +++ b/src/instrumentserver/base.py @@ -1,8 +1,9 @@ -import zmq import json import logging -from .blueprints import to_dict, deserialize_obj +import zmq + +from .blueprints import deserialize_obj, to_dict logger = logging.getLogger(__name__) diff --git a/src/instrumentserver/blueprints.py b/src/instrumentserver/blueprints.py index 7ad2bc2..446b657 100644 --- a/src/instrumentserver/blueprints.py +++ b/src/instrumentserver/blueprints.py @@ -53,21 +53,19 @@ import inspect import json import logging -from enum import Enum, unique from collections.abc import Iterable -from dataclasses import dataclass, field, fields, asdict, is_dataclass, Field -from typing import Union, Optional, List, Dict, Callable, Tuple, Any, get_args, cast +from dataclasses import asdict, dataclass, field, fields +from enum import Enum, unique +from typing import Any, Callable, Dict, List, Optional, Tuple, Union, cast, get_args import numpy as np from qcodes import ( - Station, Instrument, InstrumentChannel, Parameter, ParameterWithSetpoints, ) from qcodes.instrument.base import InstrumentBase -from qcodes.utils.validators import Validator from .helpers import objectClassPath, typeClassPath @@ -372,7 +370,7 @@ def __str__(self) -> str: ret = ret + f'\n "value":"{self.value}"' if self.unit is not None: ret = ret + f'\n "unit":"{self.unit}"' - ret = ret + f"""\n}}""" + ret = ret + """\n}""" return ret def __repr__(self): @@ -689,7 +687,7 @@ def __init__( message = message.replace("none", "null") after_json_loads = json.loads(message) self.message = after_json_loads - except json.JSONDecodeError as e: + except json.JSONDecodeError: logger.debug( f"message could not be decoded by JSON and will be treated as a string: {message}" ) @@ -870,7 +868,7 @@ def _is_numeric(val) -> Optional[Union[float, complex]]: Tries to convert the input into a int or a float. If it can, returns the conversion. Otherwise returns None. """ try: - if val is not None and not "." in val: + if val is not None and "." not in val: int_conversion = int(val) return int_conversion except Exception: @@ -931,7 +929,7 @@ def deserialize_obj(data: Any): try: loaded_json = json.loads(data.replace("'", '"')) return deserialize_obj(loaded_json) - except json.JSONDecodeError as e: + except json.JSONDecodeError: logger.debug("str could not be decoded, treating it as a str") return data diff --git a/src/instrumentserver/client/__init__.py b/src/instrumentserver/client/__init__.py index b135c9c..7253c60 100644 --- a/src/instrumentserver/client/__init__.py +++ b/src/instrumentserver/client/__init__.py @@ -1,2 +1,6 @@ -from .core import sendRequest -from .proxy import ProxyInstrument, Client, QtClient, SubClient, ClientStation +from .core import sendRequest as sendRequest +from .proxy import Client as Client +from .proxy import ClientStation as ClientStation +from .proxy import ProxyInstrument as ProxyInstrument +from .proxy import QtClient as QtClient +from .proxy import SubClient as SubClient diff --git a/src/instrumentserver/client/application.py b/src/instrumentserver/client/application.py index 4e134f7..2355679 100644 --- a/src/instrumentserver/client/application.py +++ b/src/instrumentserver/client/application.py @@ -1,24 +1,21 @@ -from pathlib import Path -from typing import Optional, Union -import sys -import json -import logging import importlib +import logging +import sys +from pathlib import Path +from typing import Union -from qcodes import Instrument -from qtpy.QtWidgets import QFileDialog, QMenu, QWidget, QSizePolicy, QSplitter from qtpy.QtGui import QGuiApplication -from qtpy.QtCore import Qt +from qtpy.QtWidgets import QFileDialog, QWidget -from instrumentserver import QtCore, QtWidgets, QtGui, getInstrumentserverPath -from instrumentserver.client import QtClient, Client, ClientStation +from instrumentserver import QtCore, QtGui, QtWidgets, getInstrumentserverPath +from instrumentserver.blueprints import ParameterBroadcastBluePrint +from instrumentserver.client import ClientStation from instrumentserver.client.proxy import SubClient from instrumentserver.gui.instruments import GenericInstrument from instrumentserver.gui.misc import DetachableTabWidget -from instrumentserver.log import LogLevels, LogWidget, log +from instrumentserver.log import LogWidget from instrumentserver.log import logger as get_instrument_logger from instrumentserver.server.application import StationList, StationObjectInfo -from instrumentserver.blueprints import ParameterBroadcastBluePrint # instrument class key in configuration files for configurations that will be applied to all instruments DEFAULT_INSTRUMENT_KEY = "__default__" diff --git a/src/instrumentserver/client/core.py b/src/instrumentserver/client/core.py index 870f018..00afeaa 100644 --- a/src/instrumentserver/client/core.py +++ b/src/instrumentserver/client/core.py @@ -1,13 +1,13 @@ import logging +import uuid import warnings + import zmq -import uuid from instrumentserver import DEFAULT_PORT -from instrumentserver.base import send, recv +from instrumentserver.base import recv, send from instrumentserver.server.core import ServerResponse - logger = logging.getLogger(__name__) @@ -88,7 +88,7 @@ def ask(self, message): try: send(self.socket, message) ret = recv(self.socket) - logger.debug(f"Response received.") + logger.debug("Response received.") logger.debug(f"Response: {str(ret)}") except zmq.error.Again: self._reset_connection() diff --git a/src/instrumentserver/client/proxy.py b/src/instrumentserver/client/proxy.py index 2f4aa03..a574c68 100644 --- a/src/instrumentserver/client/proxy.py +++ b/src/instrumentserver/client/proxy.py @@ -5,37 +5,37 @@ @author: Chao """ +import collections import inspect import json -import yaml import logging import os -from types import MethodType -import collections -from typing import Any, Union, Optional, Dict, List import threading from contextlib import contextmanager +from types import MethodType +from typing import Any, Dict, List, Optional, Union import qcodes as qc import zmq from qcodes import Instrument, Parameter from qcodes.instrument.base import InstrumentBase -from instrumentserver import QtCore, DEFAULT_PORT +from instrumentserver import DEFAULT_PORT, QtCore from instrumentserver.helpers import flat_to_nested_dict, flatten_dict, is_flat_dict from instrumentserver.server.core import ( - ServerInstruction, + CallSpec, + InstrumentCreationSpec, InstrumentModuleBluePrint, - ParameterBluePrint, MethodBluePrint, - CallSpec, Operation, - InstrumentCreationSpec, + ParameterBluePrint, ParameterSerializeSpec, + ServerInstruction, ) -from .core import sendRequest, BaseClient + from ..base import recvMultipart from ..blueprints import ParameterBroadcastBluePrint +from .core import BaseClient, sendRequest logger = logging.getLogger(__name__) @@ -285,10 +285,8 @@ def add_parameter(self, name: str, *arg, **kw): if name in self.parameters: raise ValueError(f"Parameter: {name} already present in the proxy.") - bp: InstrumentModuleBluePrint if self.cli is None: raise ValueError("No client is connected to the proxy instrument.") - bp = self.cli.getBluePrint(self.name) self.cli.call(self.name + ".add_parameter", name, *arg, **kw) self.update() @@ -298,10 +296,8 @@ def remove_parameter(self, name: str, *arg, **kw): Checking whether the paremeter exists or not is left to the instrument in the server. This is to avoid having to check on every submodule for the parameter manager. """ - bp: InstrumentModuleBluePrint if self.cli is None: raise ValueError("No client is connected to the proxy instrument.") - bp = self.cli.getBluePrint(self.name) self.cli.call(self.name + ".remove_parameter", name, *arg, **kw) self.update() @@ -921,7 +917,7 @@ def wrapper(self, *args, **kwargs): ) self.client = self._make_client(connect=True) self._create_instruments(self.full_config) - logger.info(f"Successfully remade instrument client.") + logger.info("Successfully remade instrument client.") retval = func(self, *args, **kwargs) return retval diff --git a/src/instrumentserver/config.py b/src/instrumentserver/config.py index e5417b7..cc18012 100644 --- a/src/instrumentserver/config.py +++ b/src/instrumentserver/config.py @@ -6,10 +6,10 @@ import io import tempfile -from typing import IO, Any +from pathlib import Path +from typing import IO import ruamel.yaml # type: ignore[import-untyped] # Known bugfix under no-fix status: https://sourceforge.net/p/ruamel-yaml/tickets/328/ -from pathlib import Path # Centralised point of extra fields for the server with its default as value SERVERFIELDS = {"initialize": True} @@ -73,7 +73,7 @@ def loadConfig(configPath: str | Path) -> tuple[str, dict, dict, IO[bytes], dict if "gui" in configDict: guiDict = configDict.pop("gui") if guiDict is None: - raise AttributeError(f'"gui" field cannot be None') + raise AttributeError('"gui" field cannot be None') if "type" in guiDict: if guiDict["type"] == "generic" or guiDict["type"] == "Generic": guiDict["type"] = GUIFIELD["type"] diff --git a/src/instrumentserver/gui/__init__.py b/src/instrumentserver/gui/__init__.py index 05a6a8a..664b446 100644 --- a/src/instrumentserver/gui/__init__.py +++ b/src/instrumentserver/gui/__init__.py @@ -1,4 +1,4 @@ -from .. import QtCore, QtWidgets, resource +from .. import QtCore, QtWidgets def getStyleSheet(): diff --git a/src/instrumentserver/gui/base_instrument.py b/src/instrumentserver/gui/base_instrument.py index 996cb12..47bb0e2 100644 --- a/src/instrumentserver/gui/base_instrument.py +++ b/src/instrumentserver/gui/base_instrument.py @@ -104,7 +104,7 @@ import fnmatch from pprint import pprint -from typing import Optional, List, Dict +from typing import Dict, List, Optional from instrumentserver import QtCore, QtGui, QtWidgets @@ -300,7 +300,6 @@ def addItem(self, fullName, **kwargs): :param fullName: The name of the parameter """ path = fullName.split(".")[:-1] - paramName = fullName.split(".")[-1] parent = self smName = None diff --git a/src/instrumentserver/gui/instruments.py b/src/instrumentserver/gui/instruments.py index e145d45..9815a65 100644 --- a/src/instrumentserver/gui/instruments.py +++ b/src/instrumentserver/gui/instruments.py @@ -1,28 +1,25 @@ -import json -import logging import inspect -from pprint import pprint -from typing import Optional, Any, List, Tuple, Union, Callable, Dict, Type +import logging +from typing import Callable, Dict, Optional, Union + +from qcodes import Instrument from instrumentserver.gui.misc import AlertLabelGreen -from qcodes import Parameter, Instrument -from . import parameters, keepSmallHorizontally +from .. import DEFAULT_PORT, QtCore, QtGui, QtWidgets +from ..blueprints import ParameterBroadcastBluePrint +from ..client import ProxyInstrument, SubClient +from ..helpers import nestedAttributeFromString +from ..params import ParameterManager, ParameterTypes, parameterTypes, paramTypeFromName +from . import keepSmallHorizontally from .base_instrument import ( + DelegateBase, InstrumentDisplayBase, - ItemBase, InstrumentModelBase, InstrumentTreeViewBase, - DelegateBase, + ItemBase, ) -from .parameters import ParameterWidget, AnyInput, AnyInputForMethod -from .. import QtWidgets, QtCore, QtGui, DEFAULT_PORT -from ..blueprints import ParameterBroadcastBluePrint -from ..client import ProxyInstrument, SubClient -from ..helpers import stringToArgsAndKwargs, nestedAttributeFromString -from ..params import ParameterManager, paramTypeFromName, ParameterTypes, parameterTypes -from ..serialize import toParamDict -from ast import literal_eval +from .parameters import AnyInputForMethod, ParameterWidget # TODO: all styles set through a global style sheet. # TODO: [maybe] add a column for information on valid input values? @@ -388,7 +385,7 @@ def onItemNewValue(self, itemName, value): try: # use the abstract set method defined in parameter widget so it works for different types of widgets widget._setMethod(value) - except RuntimeError as e: + except RuntimeError: logger.debug( f"Could not set value for {itemName} to {value}. Object is not being shown right now." ) diff --git a/src/instrumentserver/gui/misc.py b/src/instrumentserver/gui/misc.py index acb7cc4..732e31d 100644 --- a/src/instrumentserver/gui/misc.py +++ b/src/instrumentserver/gui/misc.py @@ -1,6 +1,6 @@ from typing import Optional, Tuple -from .. import QtWidgets, QtGui, QtCore +from .. import QtCore, QtGui, QtWidgets class AlertLabel(QtWidgets.QLabel): @@ -222,7 +222,7 @@ def onAttatchTab(self, widget, name): if name in self.unclosableTabs: self.addUnclosableTab(widget, name) else: - index = self.addTab(widget, name) + self.addTab(widget, name) @QtCore.Slot(int, int) def onMoveTab(self, fromIndex, toIndex): diff --git a/src/instrumentserver/gui/parameters.py b/src/instrumentserver/gui/parameters.py index 94e3444..f7fa9ea 100644 --- a/src/instrumentserver/gui/parameters.py +++ b/src/instrumentserver/gui/parameters.py @@ -1,15 +1,14 @@ import logging -import math import numbers -from typing import Any, Optional, List import re +from typing import Any, Callable, List, Optional from qcodes import Parameter +from .. import QtCore, QtGui, QtWidgets +from ..params import ParameterTypes, paramTypeFromVals from . import keepSmallHorizontally from .misc import AlertLabel -from .. import QtWidgets, QtCore, QtGui, resource -from ..params import ParameterTypes, paramTypeFromVals logger = logging.getLogger(__name__) @@ -91,7 +90,6 @@ def __init__( # depending on the validator of the parameter, we'll create a fitting # input widget ptype = paramTypeFromVals(parameter.vals) - vals = parameter.vals self.paramWidget: ( NumberInput | AnyInput @@ -248,7 +246,7 @@ def value(self): if self.doEval.isChecked(): try: ret = eval(self.input.text()) - except Exception as e: + except Exception: ret = self.input.text() return ret else: @@ -277,7 +275,7 @@ def __init__(self, parent=None): def checkIfNumber(self, value: str): try: val = eval(value) - except: + except Exception: val = None if not isinstance(val, numbers.Number): @@ -292,7 +290,7 @@ def checkIfNumber(self, value: str): def value(self): try: value = eval(self.text()) - except: + except Exception: return None if isinstance(value, numbers.Number): return value diff --git a/src/instrumentserver/helpers.py b/src/instrumentserver/helpers.py index 9ddc788..09bb205 100644 --- a/src/instrumentserver/helpers.py +++ b/src/instrumentserver/helpers.py @@ -1,11 +1,10 @@ import inspect -from typing import Dict, Any, List, Union, Tuple +from typing import Any, Dict, List, Tuple, Union from qcodes import Instrument, Parameter from .serialize import toParamDict - # TODO: check for usage of get params / methods functions -- might not be needed diff --git a/src/instrumentserver/log.py b/src/instrumentserver/log.py index 738df2c..4d2ea43 100644 --- a/src/instrumentserver/log.py +++ b/src/instrumentserver/log.py @@ -2,13 +2,13 @@ instrumentserver.log : Logging tools and defaults for instrumentserver. """ -import sys import logging +import re +import sys from enum import Enum, auto, unique from html import escape -import re -from . import QtGui, QtWidgets, QtCore +from . import QtCore, QtGui, QtWidgets @unique diff --git a/src/instrumentserver/monitoring/listener.py b/src/instrumentserver/monitoring/listener.py index 04ec185..1bd55b8 100644 --- a/src/instrumentserver/monitoring/listener.py +++ b/src/instrumentserver/monitoring/listener.py @@ -1,12 +1,12 @@ import argparse import logging import os.path -from abc import ABC, abstractmethod +from abc import ABC from dataclasses import dataclass -from datetime import datetime, timedelta, timezone -from zoneinfo import ZoneInfo, ZoneInfoNotFoundError +from datetime import datetime from pathlib import Path from typing import Any, Dict +from zoneinfo import ZoneInfo, ZoneInfoNotFoundError import pandas as pd import ruamel.yaml # type: ignore[import-untyped] # Known bugfix under no-fix status: https://sourceforge.net/p/ruamel-yaml/tickets/328/ diff --git a/src/instrumentserver/params.py b/src/instrumentserver/params.py index 4830b08..ccb1f15 100644 --- a/src/instrumentserver/params.py +++ b/src/instrumentserver/params.py @@ -1,10 +1,10 @@ +import json +import logging import os +from enum import Enum, auto, unique from pathlib import Path -from typing import Any, Dict, Union, List -from enum import Enum, unique, auto -import logging +from typing import Any, Dict, List, Union -import json from qcodes import Parameter from qcodes.instrument.base import InstrumentBase from qcodes.parameters import ParameterBase @@ -12,7 +12,6 @@ from . import serialize - logger = logging.getLogger(__name__) @@ -196,7 +195,7 @@ def _get_parent( def has_param(self, param_name: str): try: - param = self._get_param(param_name) + self._get_param(param_name) return True except ValueError: return False diff --git a/src/instrumentserver/serialize.py b/src/instrumentserver/serialize.py index 7934894..4a04055 100644 --- a/src/instrumentserver/serialize.py +++ b/src/instrumentserver/serialize.py @@ -68,18 +68,15 @@ import json import logging -import os -from typing import Dict, List, Any, Union -from dataclasses import fields, dataclass +from typing import Any, Dict, List, Union -from jsonschema import validate import pandas as pd -from qcodes import Instrument, Station, Parameter +from jsonschema import validate +from qcodes import Parameter, Station from qcodes.instrument.base import InstrumentBase from . import PARAMS_SCHEMA_PATH - logger = logging.getLogger(__name__) SerializableType = Union[Station, List[Union[InstrumentBase, Parameter]]] @@ -201,7 +198,7 @@ def isSimpleFormat(paramDict: Dict[str, Any]): are a dictionary with a least the key `value` in it. """ for k, v in paramDict.items(): - if not isinstance(v, dict) or not "value" in v: + if not isinstance(v, dict) or "value" not in v: return True return False diff --git a/src/instrumentserver/server/application.py b/src/instrumentserver/server/application.py index 9ae40f9..a38f6da 100644 --- a/src/instrumentserver/server/application.py +++ b/src/instrumentserver/server/application.py @@ -2,19 +2,19 @@ import importlib import logging import os -import time import sys -from typing import Union, Optional, Any, Dict +import time +from typing import Any, Dict, Optional, Union from instrumentserver.client import QtClient from instrumentserver.log import LogLevels, LogWidget, log -from .core import StationServer, InstrumentModuleBluePrint, ParameterBluePrint -from .. import QtCore, QtWidgets, QtGui, Client, getInstrumentserverPath -from ..gui.misc import DetachableTabWidget, BaseDialog -from ..gui.parameters import AnyInputForMethod -from ..gui.instruments import GenericInstrument +from .. import Client, QtCore, QtGui, QtWidgets, getInstrumentserverPath from ..config import GUIFIELD +from ..gui.instruments import GenericInstrument +from ..gui.misc import BaseDialog, DetachableTabWidget +from ..gui.parameters import AnyInputForMethod +from .core import InstrumentModuleBluePrint, ParameterBluePrint, StationServer logger = logging.getLogger(__name__) @@ -236,7 +236,7 @@ def onExceptionDialog(exception: str): dialog.setWindowTitle("Instrument Creation Error") layout = QtWidgets.QVBoxLayout(dialog) - exceptionRaisedLabel = QtWidgets.QLabel(f"Exception Raised:") + exceptionRaisedLabel = QtWidgets.QLabel("Exception Raised:") exceptionLabel = QtWidgets.QLabel(exception) accept = QtWidgets.QPushButton("Accept") @@ -298,7 +298,7 @@ def __init__(self, guiConfig: Optional[dict] = None, *args): self.setHeaderLabels(self.cols) self.basedInstrumentAction = QtWidgets.QAction( - f"Create instrument based on this" + "Create instrument based on this" ) self.basedInstrumentAction.setShortcut("N") # you need to add the action to the widget so that it can detect the shortcut @@ -956,7 +956,7 @@ def addInstrumentTab(self, item: QtWidgets.QTreeWidgetItem, index: int): kwargs = guiConfig[name]["gui"]["kwargs"] # If the instrument does not have a guiconfig an exception is raised. just use defaults values - except Exception as e: + except Exception: pass insWidget = widgetClass(ins, parent=self, **kwargs) diff --git a/src/instrumentserver/server/core.py b/src/instrumentserver/server/core.py index 57c5344..3f196cc 100644 --- a/src/instrumentserver/server/core.py +++ b/src/instrumentserver/server/core.py @@ -15,55 +15,44 @@ # TODO: client white list -import os import importlib import json import logging -import random +import os import queue +import random import socket - -from pathlib import Path -from dataclasses import dataclass, field, fields -from enum import Enum, unique -from typing import Dict, Any, Union, Optional, Tuple, List, Callable -from concurrent.futures import ThreadPoolExecutor import threading - -import zmq +from concurrent.futures import ThreadPoolExecutor +from typing import Any, Callable, Dict, List, Optional, Tuple, Union import qcodes as qc +import zmq from qcodes import ( - Station, - Instrument, - InstrumentChannel, Parameter, - ParameterWithSetpoints, + Station, ) -from qcodes.instrument.base import InstrumentBase -from qcodes.utils.validators import Validator from .. import QtCore, serialize +from ..base import recv_router, send_router, sendBroadcast from ..blueprints import ( - ParameterBluePrint, - MethodBluePrint, - InstrumentModuleBluePrint, - ParameterBroadcastBluePrint, - bluePrintFromMethod, - bluePrintFromInstrumentModule, - bluePrintFromParameter, INSTRUMENT_MODULE_BASE_CLASSES, PARAMETER_BASE_CLASSES, - Operation, - InstrumentCreationSpec, CallSpec, + InstrumentCreationSpec, + InstrumentModuleBluePrint, + MethodBluePrint, + Operation, + ParameterBluePrint, + ParameterBroadcastBluePrint, ParameterSerializeSpec, ServerInstruction, ServerResponse, + bluePrintFromInstrumentModule, + bluePrintFromMethod, + bluePrintFromParameter, ) - -from ..base import send_router, recv_router, sendBroadcast -from ..helpers import nestedAttributeFromString, objectClassPath, typeClassPath +from ..helpers import nestedAttributeFromString __author__ = "Wolfgang Pfaff", "Chao Zhou" __license__ = "MIT" @@ -212,7 +201,7 @@ def startServer(self) -> bool: """Start the server. This function does not return until the ZMQ server has been shut down.""" - logger.info(f"Starting server.") + logger.info("Starting server.") logger.info(f"The safe word is: {self.SAFEWORD}") context = zmq.Context() socket = context.socket(zmq.ROUTER) @@ -238,11 +227,11 @@ def startServer(self) -> bool: self.externalBroadcastSocket = context.socket(zmq.PUB) self.externalBroadcastSocket.bind(self.externalBroadcastAddr) else: - logger.info(f"Not broadcasting to external address") + logger.info("Not broadcasting to external address") self.serverRunning = True if self.initScript not in ["", None]: - logger.info(f"Running init script") + logger.info("Running init script") self._runInitScript() # create a thread pool for handling incoming client requests concurrently @@ -366,13 +355,13 @@ def _handleRouterMessage(self, identity, message): response_to_client = self.executeServerInstruction(instruction) response_log = f"Response to client: {str(response_to_client)}" if response_to_client.error is None: - logger.debug(f"Response sent to client.") + logger.debug("Response sent to client.") logger.debug(response_log) else: logger.warning(response_log) else: - response_log = f"Invalid message type." + response_log = "Invalid message type." response_to_client = ServerResponse(message=None, error=response_log) logger.warning(f"Invalid message type: {type(message)}.") logger.debug(f"Invalid message received: {str(message)}") diff --git a/src/instrumentserver/server/pollingWorker.py b/src/instrumentserver/server/pollingWorker.py index dbc16d0..5b81de5 100644 --- a/src/instrumentserver/server/pollingWorker.py +++ b/src/instrumentserver/server/pollingWorker.py @@ -2,7 +2,6 @@ from typing import Dict, Optional from .. import QtCore - from ..client import Client from ..helpers import nestedAttributeFromString diff --git a/src/instrumentserver/testing/dummy_instruments/generic.py b/src/instrumentserver/testing/dummy_instruments/generic.py index 128a8cf..e20c46c 100644 --- a/src/instrumentserver/testing/dummy_instruments/generic.py +++ b/src/instrumentserver/testing/dummy_instruments/generic.py @@ -1,13 +1,13 @@ # mypy: ignore-errors # No need to mypy check dummy testing instruments. +import time from typing import List +import numpy as np from qcodes import Instrument -from qcodes.utils import validators from qcodes.math_utils.field_vector import FieldVector -import numpy as np -import time +from qcodes.utils import validators class DummyChannel(Instrument): @@ -249,5 +249,5 @@ def set_complex_list(self, value): self.complex_lst = value def generic_function(self): - print(f"this generic function has been called") + print("this generic function has been called") return 3 diff --git a/src/instrumentserver/testing/dummy_instruments/rf.py b/src/instrumentserver/testing/dummy_instruments/rf.py index cec96fa..7f078eb 100644 --- a/src/instrumentserver/testing/dummy_instruments/rf.py +++ b/src/instrumentserver/testing/dummy_instruments/rf.py @@ -1,8 +1,9 @@ import numpy as np -from scipy import constants # type: ignore[import-untyped] # We don't need mypy checks for this dependency that is only used here. - from qcodes import Instrument, ParameterWithSetpoints, find_or_create_instrument from qcodes.utils import validators +from scipy import ( + constants, # type: ignore[import-untyped] # We don't need mypy checks for this dependency that is only used here. +) class ResonatorResponse(Instrument): diff --git a/test/client_workingdir/init_client.py b/test/client_workingdir/init_client.py index 41d111d..f912ca8 100755 --- a/test/client_workingdir/init_client.py +++ b/test/client_workingdir/init_client.py @@ -4,9 +4,8 @@ import os from qcodes import Instrument -from instrumentserver.client import Client -from instrumentserver.client import ProxyInstrument +from instrumentserver.client import Client # %% Create all my instruments Instrument.close_all() diff --git a/test/notebooks/Autoupdate.ipynb b/test/notebooks/Autoupdate.ipynb index 2f8bcac..6d488dd 100644 --- a/test/notebooks/Autoupdate.ipynb +++ b/test/notebooks/Autoupdate.ipynb @@ -7,16 +7,8 @@ "metadata": {}, "outputs": [], "source": [ - "from pprint import pprint\n", - "import time\n", "\n", - "import numpy as np\n", - "from matplotlib import pyplot as plt\n", - "import h5py\n", - "\n", - "\n", - "from qcodes import Instrument, Station, find_or_create_instrument\n", - "from plottr.data import datadict_storage as dds, datadict as dd" + "\n" ] }, { @@ -296,9 +288,7 @@ "id": "c8ee480c-13a5-4941-b3d2-d6dd85c359ac", "metadata": {}, "outputs": [], - "source": [ - "import zmq" - ] + "source": [] }, { "cell_type": "code", diff --git a/test/notebooks/Prototype the ParamManager.ipynb b/test/notebooks/Prototype the ParamManager.ipynb index 0953edd..ef89282 100644 --- a/test/notebooks/Prototype the ParamManager.ipynb +++ b/test/notebooks/Prototype the ParamManager.ipynb @@ -30,27 +30,16 @@ }, "outputs": [], "source": [ - "import numpy as np\n", - "import random\n", - "import math\n", - "import numbers\n", - "from pprint import pprint\n", "\n", - "from qcodes import Station, Instrument, Parameter\n", + "from qcodes import Instrument, Station\n", "from qcodes.utils import validators\n", "\n", - "from instrumentserver import setupLogging, servergui\n", - "from instrumentserver.helpers import getInstrumentMethods, getInstrumentParameters\n", - "from instrumentserver.serialize import (\n", - " toDataFrame,\n", - " saveParamsToFile,\n", - " loadParamsFromFile,\n", - " toParamDict,\n", - ")\n", - "\n", "from instrumentserver.gui import widgetDialog\n", + "from instrumentserver.gui.instruments import ParameterManagerGui\n", "from instrumentserver.params import ParameterManager\n", - "from instrumentserver.gui.instruments import ParameterManagerGui" + "from instrumentserver.serialize import (\n", + " toDataFrame,\n", + ")" ] }, { diff --git a/test/notebooks/Run the station server from a notebook.ipynb b/test/notebooks/Run the station server from a notebook.ipynb index 9ecbdd1..24679da 100644 --- a/test/notebooks/Run the station server from a notebook.ipynb +++ b/test/notebooks/Run the station server from a notebook.ipynb @@ -19,7 +19,7 @@ }, "outputs": [], "source": [ - "from qcodes import Instrument, Station, Parameter\n", + "from qcodes import Instrument, Parameter, Station\n", "\n", "from instrumentserver import servergui" ] diff --git a/test/prototyping/server_admin.py b/test/prototyping/server_admin.py index 3885983..aefbc52 100755 --- a/test/prototyping/server_admin.py +++ b/test/prototyping/server_admin.py @@ -1,13 +1,14 @@ # -*- coding: utf-8 -*- -import logging # %% imports -from qcodes import Instrument -from instrumentserver.server import * -from instrumentserver.client import * from pprint import pprint +from qcodes import Instrument + +from instrumentserver.client import Client +from instrumentserver.server import * # noqa: F401,F403 + # from instrumentserver import log # log.setupLogging(addStreamHandler=True, streamHandlerLevel=logging.DEBUG) # logger = log.logger('instrumentserver') diff --git a/test/prototyping/testing_parameter_manager.py b/test/prototyping/testing_parameter_manager.py index 5d669ce..9161dc9 100755 --- a/test/prototyping/testing_parameter_manager.py +++ b/test/prototyping/testing_parameter_manager.py @@ -1,19 +1,13 @@ # -*- coding: utf-8 -*- # %% imports -import inspect -import numpy as np -from qcodes import Station, Instrument -from qcodes.utils import validators - -from instrumentserver import QtWidgets +from qcodes import Instrument, Station +from instrumentserver.client import Client, ProxyInstrument from instrumentserver.gui import widgetDialog -from instrumentserver.params import ParameterManager from instrumentserver.gui.instruments import ParameterManagerGui -from instrumentserver.client import Client, ProxyInstrument - +from instrumentserver.params import ParameterManager # %% run the PM locally Instrument.close_all() diff --git a/test/pytest/conftest.py b/test/pytest/conftest.py index 6aa83d6..8abfed9 100644 --- a/test/pytest/conftest.py +++ b/test/pytest/conftest.py @@ -1,10 +1,9 @@ -import instrumentserver.testing.dummy_instruments.generic import pytest # type: ignore[import-not-found] import qcodes as qc -from instrumentserver.server.core import startServer from instrumentserver.client.core import BaseClient from instrumentserver.client.proxy import Client +from instrumentserver.server.core import startServer @pytest.fixture(autouse=True, scope="module") diff --git a/test/pytest/test_base.py b/test/pytest/test_base.py index 54e26f4..8bdc51a 100644 --- a/test/pytest/test_base.py +++ b/test/pytest/test_base.py @@ -10,22 +10,19 @@ import zmq from instrumentserver.base import ( - encode, decode, - send, + encode, recv, - sendBroadcast, recvMultipart, + send, + sendBroadcast, ) from instrumentserver.blueprints import ( + Operation, ParameterBroadcastBluePrint, ServerInstruction, - Operation, - bluePrintToDict, - deserialize_obj, ) - # --------------------------------------------------------------------------- # encode / decode (blueprint objects only) # --------------------------------------------------------------------------- diff --git a/test/pytest/test_basic_functionality.py b/test/pytest/test_basic_functionality.py index 9855209..e5be6e9 100644 --- a/test/pytest/test_basic_functionality.py +++ b/test/pytest/test_basic_functionality.py @@ -12,11 +12,11 @@ def test_creating_and_accessing_param(param_manager): def test_getting_all_instruments(cli): - dummy = cli.find_or_create_instrument( + cli.find_or_create_instrument( "dummy", "instrumentserver.testing.dummy_instruments.generic.DummyInstrumentWithSubmodule", ) - params = cli.find_or_create_instrument( + cli.find_or_create_instrument( "parameter_manager", "instrumentserver.params.ParameterManager" ) all_ins = cli.list_instruments() diff --git a/test/pytest/test_client_station.py b/test/pytest/test_client_station.py index da10e7f..db5fb8f 100644 --- a/test/pytest/test_client_station.py +++ b/test/pytest/test_client_station.py @@ -1,7 +1,5 @@ """Tests for ClientStation and ClientStationGui.""" -import json -from pathlib import Path import pytest @@ -25,7 +23,7 @@ def client_station(start_server): def test_client_station_creates_instruments(client_station): - ins = client_station.find_or_create_instrument("cs_dummy", DUMMY_CLASS) + client_station.find_or_create_instrument("cs_dummy", DUMMY_CLASS) assert "cs_dummy" in client_station.instruments @@ -121,7 +119,6 @@ def test_client_station_gui_server_widget_shows_host_port(qtbot, start_server): def test_client_station_gui_station_list_populated(qtbot, start_server): from instrumentserver.client.application import ClientStationGui - from instrumentserver import QtCore station = ClientStation(host="localhost", port=5555) station.find_or_create_instrument("gui_cs_dummy", DUMMY_CLASS) @@ -135,9 +132,9 @@ def test_client_station_gui_station_list_populated(qtbot, start_server): def test_client_station_gui_open_instrument_tab(qtbot, start_server): + from instrumentserver import QtCore from instrumentserver.client.application import ClientStationGui from instrumentserver.gui.instruments import GenericInstrument - from instrumentserver import QtCore station = ClientStation(host="localhost", port=5555) station.find_or_create_instrument("gui_cs_dummy2", DUMMY_CLASS) diff --git a/test/pytest/test_config.py b/test/pytest/test_config.py index 82d63e0..fdbe570 100644 --- a/test/pytest/test_config.py +++ b/test/pytest/test_config.py @@ -1,9 +1,10 @@ """Tests for instrumentserver.config.loadConfig.""" -import pytest from pathlib import Path -from instrumentserver.config import loadConfig, GUIFIELD +import pytest + +from instrumentserver.config import GUIFIELD, loadConfig def _write_config(tmp_path: Path, content: str) -> Path: diff --git a/test/pytest/test_helpers.py b/test/pytest/test_helpers.py index c284210..bd7a44e 100644 --- a/test/pytest/test_helpers.py +++ b/test/pytest/test_helpers.py @@ -1,16 +1,15 @@ import pytest from instrumentserver.helpers import ( - stringToArgsAndKwargs, flat_to_nested_dict, flatten_dict, is_flat_dict, nestedAttributeFromString, - typeClassPath, objectClassPath, + stringToArgsAndKwargs, + typeClassPath, ) - # --------------------------------------------------------------------------- # stringToArgsAndKwargs # --------------------------------------------------------------------------- diff --git a/test/pytest/test_json_serializable.py b/test/pytest/test_json_serializable.py index 36ff5bf..1916b57 100644 --- a/test/pytest/test_json_serializable.py +++ b/test/pytest/test_json_serializable.py @@ -3,15 +3,14 @@ from qcodes.math_utils.field_vector import FieldVector from instrumentserver.blueprints import ( + ParameterBroadcastBluePrint, bluePrintFromInstrumentModule, bluePrintFromMethod, bluePrintFromParameter, bluePrintToDict, deserialize_obj, - ParameterBroadcastBluePrint, - iterable_to_serialized_dict, dict_to_serialized_dict, - to_dict, + iterable_to_serialized_dict, ) from instrumentserver.testing.dummy_instruments.generic import ( DummyInstrumentWithSubmodule, @@ -44,7 +43,7 @@ def __init__(self, x=1, y=2, z=3): self.z = z def customFunction(self, x: int, y: int) -> int: - print(f"I am in my function") + print("I am in my function") return x * y diff --git a/test/pytest/test_param_manager.py b/test/pytest/test_param_manager.py index 4fc4549..67b168b 100644 --- a/test/pytest/test_param_manager.py +++ b/test/pytest/test_param_manager.py @@ -44,6 +44,17 @@ def test_param(param_manager): assert params.my_param() == 456 +def test_proxy_add_remove_parameter(param_manager): + cli, params = param_manager + + params.add_parameter(name="probe_param", initial_value=42, unit="V") + assert "probe_param" in params.parameters + assert params.probe_param() == 42 + + params.remove_parameter(name="probe_param") + assert "probe_param" not in params.parameters + + def test_removing_all_params(): params = ParameterManager(name="params") diff --git a/test/pytest/test_serialize.py b/test/pytest/test_serialize.py index b8764ef..05f357e 100644 --- a/test/pytest/test_serialize.py +++ b/test/pytest/test_serialize.py @@ -1,4 +1,5 @@ -from qcodes import Station, Instrument, Parameter +from qcodes import Parameter, Station + from instrumentserver.serialize import toParamDict diff --git a/test/pytest/test_server_gui.py b/test/pytest/test_server_gui.py index daf0e58..435e0cb 100644 --- a/test/pytest/test_server_gui.py +++ b/test/pytest/test_server_gui.py @@ -38,7 +38,7 @@ def test_saving_button(qtbot): } window = startServerGuiApplication() - rr = window.client.find_or_create_instrument( + window.client.find_or_create_instrument( "rr", "instrumentserver.testing.dummy_instruments.rf.ResonatorResponse" ) @@ -100,7 +100,7 @@ def test_refresh_button(qtbot): try: assert window.stationList.topLevelItemCount() == 0 - dummy = window.client.find_or_create_instrument( + window.client.find_or_create_instrument( "dummy", "instrumentserver.testing.dummy_instruments.generic.DummyInstrumentWithSubmodule", ) @@ -121,7 +121,7 @@ def test_clicking_an_item(qtbot): try: assert window.stationList.topLevelItemCount() == 0 - dummy = window.client.find_or_create_instrument( + window.client.find_or_create_instrument( "dummy", "instrumentserver.testing.dummy_instruments.generic.DummyInstrumentWithSubmodule", ) @@ -142,7 +142,7 @@ def test_opening_new_tab_generic_object(qtbot): window = startServerGuiApplication() qtbot.addWidget(window) try: - dummy = window.client.find_or_create_instrument( + window.client.find_or_create_instrument( "dummy", "instrumentserver.testing.dummy_instruments.generic.DummyInstrumentWithSubmodule", ) diff --git a/test/test_async_requests/client_station_gui.py b/test/test_async_requests/client_station_gui.py index 314d48b..490704b 100644 --- a/test/test_async_requests/client_station_gui.py +++ b/test/test_async_requests/client_station_gui.py @@ -1,9 +1,9 @@ import sys from pathlib import Path -from instrumentserver.client import ClientStation -from instrumentserver.client.application import ClientStationGui from instrumentserver import QtWidgets +from instrumentserver.client import ClientStation +from instrumentserver.client.application import ClientStationGui if __name__ == "__main__": # gather some test config files diff --git a/test/test_async_requests/demo_concurrency.py b/test/test_async_requests/demo_concurrency.py index ff626fb..e2080d9 100644 --- a/test/test_async_requests/demo_concurrency.py +++ b/test/test_async_requests/demo_concurrency.py @@ -1,7 +1,8 @@ -from instrumentserver.client import Client import sys import time +from instrumentserver.client import Client + """ Simple concurrency demo. diff --git a/test/test_async_requests/test_client.py b/test/test_async_requests/test_client.py index 309b5c4..1509faf 100644 --- a/test/test_async_requests/test_client.py +++ b/test/test_async_requests/test_client.py @@ -1,6 +1,5 @@ from instrumentserver.client import Client - """ A simple script for testing the new features on the server/client. From c8e0e636ad655c64165b1bba1336c073cce13c76 Mon Sep 17 00:00:00 2001 From: marcosf2 Date: Fri, 24 Apr 2026 17:38:21 -0300 Subject: [PATCH 07/12] First pass at mypy errors --- pyproject.toml | 3 +- src/instrumentserver/apps.py | 7 +- src/instrumentserver/base.py | 19 +- src/instrumentserver/blueprints.py | 63 +++-- src/instrumentserver/client/application.py | 56 ++-- src/instrumentserver/client/core.py | 45 ++-- src/instrumentserver/client/proxy.py | 212 ++++++++------- src/instrumentserver/config.py | 2 +- src/instrumentserver/gui/__init__.py | 22 +- src/instrumentserver/gui/base_instrument.py | 250 +++++++++--------- src/instrumentserver/gui/instruments.py | 182 +++++++------ src/instrumentserver/gui/misc.py | 92 ++++--- src/instrumentserver/gui/parameters.py | 38 +-- src/instrumentserver/helpers.py | 12 +- src/instrumentserver/log.py | 39 +-- src/instrumentserver/monitoring/listener.py | 47 ++-- src/instrumentserver/params.py | 66 +++-- src/instrumentserver/serialize.py | 13 +- src/instrumentserver/server/application.py | 243 +++++++++-------- src/instrumentserver/server/core.py | 36 +-- src/instrumentserver/server/pollingWorker.py | 14 +- .../testing/dummy_instruments/rf.py | 33 ++- 22 files changed, 820 insertions(+), 674 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 730ab47..c43b5f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,6 +56,7 @@ extend-select = ["I"] [tool.mypy] files = ["src"] +exclude = ["src/instrumentserver/resource\\.py$"] strict_optional = true show_column_numbers = true warn_unused_ignores = true @@ -74,8 +75,6 @@ module = [ "qcodes.*", "qtpy", "qtpy.*", - "pyqtgraph", - "pyqtgraph.*", "scipy", "scipy.*", "zmq", diff --git a/src/instrumentserver/apps.py b/src/instrumentserver/apps.py index 2a1c2ec..0d881d1 100644 --- a/src/instrumentserver/apps.py +++ b/src/instrumentserver/apps.py @@ -3,6 +3,7 @@ import os import signal from pathlib import Path +from typing import Any, Optional from instrumentserver.server.application import DetachedServerGui @@ -22,7 +23,7 @@ logger.setLevel(logging.INFO) -def server(**kwargs): +def server(**kwargs: Any) -> int: app = QtCore.QCoreApplication([]) # this allows us to kill the server by KeyboardInterrupt @@ -33,7 +34,7 @@ def server(**kwargs): return app.exec_() -def serverWithGui(**kwargs): +def serverWithGui(**kwargs: Any) -> int: app = QtWidgets.QApplication([]) startServerGuiApplication(**kwargs) return app.exec_() @@ -166,7 +167,7 @@ def clientStationScript() -> None: app = QtWidgets.QApplication([]) - config_path = args.config if args.config else None + config_path: Optional[str] = args.config if args.config else None station = ClientStation(host=args.host, port=args.port, config_path=config_path) window = ClientStationGui(station) window.show() diff --git a/src/instrumentserver/base.py b/src/instrumentserver/base.py index 9987514..b5623ef 100644 --- a/src/instrumentserver/base.py +++ b/src/instrumentserver/base.py @@ -1,5 +1,6 @@ import json import logging +from typing import Any, Tuple, Union import zmq @@ -8,15 +9,15 @@ logger = logging.getLogger(__name__) -def encode(data): +def encode(data: Any) -> str: return json.dumps(to_dict(data)) -def decode(data): +def decode(data: Union[str, bytes]) -> Any: return deserialize_obj(json.loads(data)) -def send(socket, data, use_string=True): +def send(socket: "zmq.Socket", data: Any, use_string: bool = True) -> Any: payload = encode(data) if use_string: return socket.send_string(payload) @@ -24,12 +25,12 @@ def send(socket, data, use_string=True): return socket.send(payload.encode("utf-8")) -def recv(socket): +def recv(socket: "zmq.Socket") -> Any: # Try multipart receive first (ROUTER replies) parts = socket.recv_multipart() while socket.getsockopt(zmq.RCVMORE): leftover = socket.recv() - logger.warning(f"Additional part found in recv: {leftover}") + logger.warning(f"Additional part found in recv: {leftover!r}") if len(parts) == 1: data = parts[0] elif len(parts) == 2 and parts[0] == b"": # optional empty delimiter @@ -39,14 +40,14 @@ def recv(socket): return decode(data) -def send_router(socket, identity, message): +def send_router(socket: "zmq.Socket", identity: bytes, message: Any) -> None: socket.setsockopt(zmq.SNDTIMEO, 5000) socket.setsockopt(zmq.LINGER, 0) payload = encode(message).encode("utf-8") socket.send_multipart([identity, b"", payload]) -def recv_router(socket): +def recv_router(socket: "zmq.Socket") -> Tuple[bytes, Any]: parts = socket.recv_multipart() if len(parts) == 2: identity, payload = parts @@ -57,7 +58,7 @@ def recv_router(socket): return identity, decode(payload) -def sendBroadcast(socket, name, message): +def sendBroadcast(socket: "zmq.Socket", name: str, message: Any) -> None: """ broadcasts the message. It will send 2 messages: First the name with the send more flag, followed by the message. @@ -70,7 +71,7 @@ def sendBroadcast(socket, name, message): socket.send(encode(message).encode("utf-8")) -def recvMultipart(socket): +def recvMultipart(socket: "zmq.Socket") -> Tuple[str, Any]: """ Recieves the broadcast from a broadcast message. It should consist of 2 parts: The first item is the name of the object sending it. Second part the message diff --git a/src/instrumentserver/blueprints.py b/src/instrumentserver/blueprints.py index 446b657..61e28fd 100644 --- a/src/instrumentserver/blueprints.py +++ b/src/instrumentserver/blueprints.py @@ -100,7 +100,7 @@ def __repr__(self) -> str: def __str__(self) -> str: return f"{self.name}: {self.parameter_class}" - def tostr(self, indent=0): + def tostr(self, indent: int = 0) -> str: i = indent * " " ret = f"""{self.name}: {self.parameter_class} {i}- unit: {self.unit} @@ -112,7 +112,7 @@ def tostr(self, indent=0): """ return ret - def toJson(self): + def toJson(self) -> Dict[str, Any]: return bluePrintToDict(self) @@ -158,13 +158,13 @@ class MethodBluePrint: docstring: str = "" _class_type: str = "MethodBluePrint" - def __repr__(self): + def __repr__(self) -> str: return str(self) - def __str__(self): + def __str__(self) -> str: return f"{self.name}{str(self.call_signature_str)}" - def tostr(self, indent=0): + def tostr(self, indent: int = 0) -> str: i = indent * " " ret = f"""{self.name}{str(self.call_signature_str)} {i}- path: {self.path} @@ -182,7 +182,7 @@ def signature_str_and_params_from_obj( param_dict[name] = str(param.kind) return call_signature_str, param_dict - def toJson(self): + def toJson(self) -> Dict[str, Any]: return bluePrintToDict(self) @@ -275,27 +275,27 @@ def __repr__(self) -> str: def __str__(self) -> str: return f"{self.name}: {self.instrument_module_class}" - def tostr(self, indent=0): + def tostr(self, indent: int = 0) -> str: i = indent * " " ret = f"""{i}{self.name}: {self.instrument_module_class} {i}- path: {self.path} {i}- base class: {self.base_class} """ ret += f"{i}- Parameters:\n{i} -----------\n" - for pn, p in self.parameters.items(): + for pn, p in self.parameters.items(): # type: ignore[union-attr] ret += f"{i} - " + p.tostr(indent + 4) ret += f"{i}- Methods:\n{i} --------\n" - for mn, m in self.methods.items(): + for mn, m in self.methods.items(): # type: ignore[union-attr] ret += f"{i} - " + m.tostr(indent + 4) ret += f"{i}- Submodules:\n{i} -----------\n" - for sn, s in self.submodules.items(): + for sn, s in self.submodules.items(): # type: ignore[union-attr] ret += f"{i} - " + s.tostr(indent + 4) return ret - def toJson(self): + def toJson(self) -> Dict[str, Any]: return bluePrintToDict(self) @@ -373,21 +373,22 @@ def __str__(self) -> str: ret = ret + """\n}""" return ret - def __repr__(self): + def __repr__(self) -> str: return str(self) - def pprint(self, indent=0): + def pprint(self, indent: int = 0) -> str: i = indent * " " + bp_type = self.bp_type # type: ignore[attr-defined] ret = f"""name: {self.name} {i}- action: {self.action} {i}- value: {self.value} {i}- unit: {self.unit} -{i}- bp_type: {self.bp_type} +{i}- bp_type: {bp_type} """ return ret - def toJson(self): + def toJson(self) -> Dict[str, Any]: return bluePrintToDict(self) @@ -414,7 +415,7 @@ def _dictToJson(_dict: dict, json_type: bool = True) -> dict: return ret -def bluePrintToDict(bp: BluePrintType, json_type=True) -> dict: +def bluePrintToDict(bp: BluePrintType, json_type: bool = True) -> dict: """ Converts a blueprint into a dictionary. @@ -484,7 +485,7 @@ class InstrumentCreationSpec: _class_type: str = "InstrumentCreationSpec" - def toJson(self): + def toJson(self) -> Dict[str, Any]: ret = asdict(self) ret["args"] = iterable_to_serialized_dict(self.args) ret["kwargs"] = dict_to_serialized_dict(self.kwargs) @@ -507,7 +508,7 @@ class CallSpec: _class_type: str = "CallSpec" - def toJson(self): + def toJson(self) -> Dict[str, Any]: ret = asdict(self) ret["args"] = iterable_to_serialized_dict(self.args) ret["kwargs"] = dict_to_serialized_dict(self.kwargs) @@ -532,7 +533,7 @@ class ParameterSerializeSpec: _class_type: str = "ParameterSerializeSpec" - def toJson(self): + def toJson(self) -> Dict[str, Any]: ret = asdict(self) ret["args"] = iterable_to_serialized_dict(self.args) ret["kwargs"] = dict_to_serialized_dict(self.kwargs) @@ -602,7 +603,7 @@ class ServerInstruction: _class_type: str = "ServerInstruction" - def validate(self): + def validate(self) -> None: if self.operation is Operation.create_instrument: if not isinstance(self.create_instrument_spec, InstrumentCreationSpec): raise ValueError("Invalid instrument creation spec.") @@ -615,8 +616,8 @@ def validate(self): if not isinstance(self.requested_path, str): raise ValueError("Invalid requested path.") - def toJson(self): - ret = {"operation": str(self.operation.name)} + def toJson(self) -> Dict[str, Any]: + ret: Dict[str, Any] = {"operation": str(self.operation.name)} if self.create_instrument_spec is None: ret["create_instrument_spec"] = None @@ -698,8 +699,8 @@ def __init__( self._class_type = "ServerResponse" - def toJson(self): - ret = {} + def toJson(self) -> Dict[str, Any]: + ret: Dict[str, Any] = {} if isinstance(self.message, get_args(BluePrintType)): ret["message"] = self.message.toJson() elif hasattr(self.message, "attributes"): @@ -769,7 +770,9 @@ def _convert_dict_to_obj(item_dict: dict) -> Any: return instantiated_obj -def iterable_to_serialized_dict(iterable: Optional[Iterable[Any]] = None): +def iterable_to_serialized_dict( + iterable: Optional[Iterable[Any]] = None, +) -> Any: """ Goes through an iterable (lists, tuples, sets) and serialize each object inside of it. If trying to serialize an arbitrary object, this object must have a class attribute "attributes" for the serialization to happen correctly. @@ -817,7 +820,9 @@ def iterable_to_serialized_dict(iterable: Optional[Iterable[Any]] = None): return converted_iterable -def dict_to_serialized_dict(dct: Optional[Dict[str, Any]] = None): +def dict_to_serialized_dict( + dct: Optional[Dict[str, Any]] = None, +) -> Any: """ Same idea as iterable_to_serialized_dict but for dictionaries. """ @@ -852,7 +857,7 @@ def dict_to_serialized_dict(dct: Optional[Dict[str, Any]] = None): return converted_dict -def to_dict(data) -> Union[Dict[str, str], str]: +def to_dict(data: Any) -> Union[Dict[str, str], str]: """ Converts object to json serializable. This is done by calling the method toJson of the object being passed. Strings are returned without any more processing. @@ -863,7 +868,7 @@ def to_dict(data) -> Union[Dict[str, str], str]: return data.toJson() -def _is_numeric(val) -> Optional[Union[float, complex]]: +def _is_numeric(val: Any) -> Optional[Union[float, complex]]: """ Tries to convert the input into a int or a float. If it can, returns the conversion. Otherwise returns None. """ @@ -889,7 +894,7 @@ def _is_numeric(val) -> Optional[Union[float, complex]]: return None -def deserialize_obj(data: Any): +def deserialize_obj(data: Any) -> Any: """ Tries to deserialize any object. If the object is a dictionary and contains the key '_class_type' it means that that dictionary represents a serialized object that needs to be instantiated. The function will try and deserailize diff --git a/src/instrumentserver/client/application.py b/src/instrumentserver/client/application.py index 2355679..7557220 100644 --- a/src/instrumentserver/client/application.py +++ b/src/instrumentserver/client/application.py @@ -2,7 +2,7 @@ import logging import sys from pathlib import Path -from typing import Union +from typing import Dict, Optional, Union from qtpy.QtGui import QGuiApplication from qtpy.QtWidgets import QFileDialog, QWidget @@ -25,7 +25,11 @@ class ServerWidget(QtWidgets.QWidget): - def __init__(self, client_station: ClientStation, parent=None): + def __init__( + self, + client_station: ClientStation, + parent: Optional[QtWidgets.QWidget] = None, + ) -> None: super().__init__(parent) self.client_station = client_station @@ -34,8 +38,8 @@ def __init__(self, client_station: ClientStation, parent=None): form_layout = QtWidgets.QFormLayout(form) form_layout.setContentsMargins(0, 0, 0, 0) form_layout.setSpacing(8) - form_layout.setLabelAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) - form_layout.setFieldGrowthPolicy(QtWidgets.QFormLayout.AllNonFixedFieldsGrow) + form_layout.setLabelAlignment(QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter) # type: ignore[arg-type] + form_layout.setFieldGrowthPolicy(QtWidgets.QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) # Non-editable self.host = QtWidgets.QLineEdit(self.client_station._host) @@ -80,7 +84,7 @@ def __init__(self, client_station: ClientStation, parent=None): # main.addLayout(btns) main.addStretch(1) - def _tint_readonly(self, le, bg="#f3f6fa"): + def _tint_readonly(self, le: QtWidgets.QLineEdit, bg: str = "#f3f6fa") -> None: pal = le.palette() pal.setColor(QtGui.QPalette.Base, QtGui.QColor(bg)) le.setPalette(pal) @@ -122,14 +126,14 @@ def __init__(self, station: ClientStation): instruments=None, sub_host=self.cli.host, sub_port=self.cli.port + 1 ) self.listener.moveToThread(self.listenerThread) - self.listenerThread.started.connect(self.listener.connect) + self.listenerThread.started.connect(self.listener.connect) # type: ignore[arg-type] self.listener.finished.connect(self.listenerThread.quit) self.listener.finished.connect(self.listener.deleteLater) self.listener.finished.connect(self.listenerThread.deleteLater) self.listener.update.connect(self.listenerEvent) self.listenerThread.start() - self.instrumentTabsOpen = {} + self.instrumentTabsOpen: Dict[str, QtWidgets.QWidget] = {} # --- main tabs self.tabs = DetachableTabWidget() @@ -147,7 +151,7 @@ def __init__(self, station: ClientStation): self.stationList.itemDoubleClicked.connect(self.openInstrumentTab) self.stationList.closeRequested.connect(self.closeInstrument) - stationWidgets = QtWidgets.QSplitter(QtCore.Qt.Horizontal) + stationWidgets = QtWidgets.QSplitter(QtCore.Qt.Orientation.Horizontal) stationWidgets.addWidget(self.stationList) stationWidgets.addWidget(self.stationObjInfo) stationWidgets.setSizes([200, 500]) @@ -165,17 +169,19 @@ def __init__(self, station: ClientStation): self.tabs.addUnclosableTab(self.server_widget, "Server") # adjust window size - screen_geometry = QGuiApplication.primaryScreen().availableGeometry() + screen_geometry = QGuiApplication.primaryScreen().availableGeometry() # type: ignore[union-attr] width = int(screen_geometry.width() * 0.3) # 30% of screen width height = int(screen_geometry.height() * 0.7) # 70% of screen height self.resize(width, height) @QtCore.Slot(ParameterBroadcastBluePrint) - def listenerEvent(self, message: ParameterBroadcastBluePrint): + def listenerEvent(self, message: ParameterBroadcastBluePrint) -> None: if message.action == "parameter-update": logger.info(f"{message.action}: {message.name}: {message.value}") - def openInstrumentTab(self, item: QtWidgets.QListWidgetItem, index: int): + def openInstrumentTab( + self, item: QtWidgets.QTreeWidgetItem, index: int + ) -> None: """ Gets called when the user double clicks and item of the instrument list. Adds a new generic instrument GUI window to the tab bar. @@ -224,7 +230,7 @@ def openInstrumentTab(self, item: QtWidgets.QListWidgetItem, index: int): self.tabs.setCurrentWidget(self.instrumentTabsOpen[name]) @QtCore.Slot(str) - def _displayComponentInfo(self, name: Union[str, None]): + def _displayComponentInfo(self, name: Union[str, None]) -> None: if name is not None: bp = self.station[name].bp else: @@ -232,25 +238,25 @@ def _displayComponentInfo(self, name: Union[str, None]): self.stationObjInfo.setObject(bp) @QtCore.Slot(int) - def onTabChanged(self, index): + def onTabChanged(self, index: int) -> None: widget = self.tabs.widget(index) # if instrument tab is not in 'instrumentTabsOpen' yet, tab must be just open, in this case the constructor # of the parameter widget should have already called refresh, so we don't have to do that again. if hasattr(widget, "parametersList") and ( - widget.objectName() in self.instrumentTabsOpen + widget.objectName() in self.instrumentTabsOpen # type: ignore[union-attr] ): - widget.parametersList.model.refreshAll() + widget.parametersList.model.refreshAll() # type: ignore[union-attr] @QtCore.Slot(str) def onTabDeleted(self, name: str) -> None: if name in self.instrumentTabsOpen: del self.instrumentTabsOpen[name] - def addParameterLoadSaveToolbar(self): + def addParameterLoadSaveToolbar(self) -> None: # --- toolbar basics --- self.toolBar = QtWidgets.QToolBar("Params", self) self.toolBar.setIconSize(QtCore.QSize(22, 22)) - self.toolBar.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon) + self.toolBar.setToolButtonStyle(QtCore.Qt.ToolButtonStyle.ToolButtonTextBesideIcon) self.addToolBar(self.toolBar) # --- composite path widget @@ -296,7 +302,7 @@ def addParameterLoadSaveToolbar(self): self.paramPathEdit.returnPressed.connect(self.loadParams) @QtCore.Slot() - def browseParamPath(self): + def browseParamPath(self) -> None: filePath, _ = QFileDialog.getOpenFileName( self, "Select Parameter File", ".", "JSON Files (*.json);;All Files (*)" ) @@ -304,7 +310,7 @@ def browseParamPath(self): self.paramPathEdit.setText(filePath) @QtCore.Slot() - def saveParams(self): + def saveParams(self) -> None: file_path = self.paramPathEdit.text() if not file_path: QtWidgets.QMessageBox.warning( @@ -318,7 +324,7 @@ def saveParams(self): QtWidgets.QMessageBox.critical(self, "Save Error", str(e)) @QtCore.Slot() - def loadParams(self): + def loadParams(self) -> None: file_path = self.paramPathEdit.text() if not file_path: QtWidgets.QMessageBox.warning( @@ -333,15 +339,15 @@ def loadParams(self): for i in range(self.tabs.count()): widget = self.tabs.widget(i) if hasattr(widget, "parametersList") and hasattr( - widget.parametersList, "model" + widget.parametersList, "model" # type: ignore[union-attr] ): - widget.parametersList.model.refreshAll() + widget.parametersList.model.refreshAll() # type: ignore[union-attr] except Exception as e: QtWidgets.QMessageBox.critical(self, "Load Error", str(e)) @QtCore.Slot(str) - def closeInstrument(self, name: str): # , item: QtWidgets.QListWidgetItem): + def closeInstrument(self, name: str) -> None: # , item: QtWidgets.QListWidgetItem): try: # close instrument on server self.station.close_instrument(name) @@ -356,7 +362,7 @@ def closeInstrument(self, name: str): # , item: QtWidgets.QListWidgetItem): logger.info(f"Closed instrument '{name}'") - def removeInstrumentFromGui(self, name: str): + def removeInstrumentFromGui(self, name: str) -> None: """Remove an instrument from the station list.""" self.stationList.removeObject(name) self.stationObjInfo.clear() @@ -364,7 +370,7 @@ def removeInstrumentFromGui(self, name: str): self.tabs.removeTab(self.tabs.indexOf(self.instrumentTabsOpen[name])) del self.instrumentTabsOpen[name] - def closeEvent(self, event: QtGui.QCloseEvent) -> None: + def closeEvent(self, event: QtGui.QCloseEvent) -> None: # type: ignore[override] """Cleanup listener thread before closing the window.""" for name, widget in list(self.instrumentTabsOpen.items()): try: diff --git a/src/instrumentserver/client/core.py b/src/instrumentserver/client/core.py index 00afeaa..a96c913 100644 --- a/src/instrumentserver/client/core.py +++ b/src/instrumentserver/client/core.py @@ -1,6 +1,8 @@ import logging import uuid import warnings +from types import TracebackType +from typing import Any, Optional, Type import zmq @@ -26,16 +28,16 @@ class BaseClient: def __init__( self, - host="localhost", - port=DEFAULT_PORT, - connect=True, - timeout=20, - raise_exceptions=True, - ): + host: str = "localhost", + port: int = DEFAULT_PORT, + connect: bool = True, + timeout: float = 20, + raise_exceptions: bool = True, + ) -> None: self.connected = False self._closed = False - self.context = None - self.socket = None + self.context: Optional[zmq.Context] = None + self.socket: Optional[zmq.Socket] = None self.host = host self.port = port self.addr = f"tcp://{host}:{port}" @@ -45,15 +47,20 @@ def __init__( if connect: self.connect() - def __enter__(self): + def __enter__(self) -> "BaseClient": if not self.connected: self.connect() return self - def __exit__(self, exc_type, exc_val, exc_tb): + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: self.disconnect() - def connect(self): + def connect(self) -> None: if self._closed: raise RuntimeError("Client has been permanently disconnected.") # Clean up any existing context/socket so we don't leak them when @@ -80,14 +87,14 @@ def connect(self): self.socket.connect(self.addr) self.connected = True - def ask(self, message): + def ask(self, message: Any) -> Any: if self._closed or not self.connected: raise RuntimeError("No connection yet.") # try so that if timeout happens, the client remains usable try: - send(self.socket, message) - ret = recv(self.socket) + send(self.socket, message) # type: ignore[arg-type] + ret = recv(self.socket) # type: ignore[arg-type] logger.debug("Response received.") logger.debug(f"Response: {str(ret)}") except zmq.error.Again: @@ -106,7 +113,7 @@ def ask(self, message): return ret - def _reset_connection(self): + def _reset_connection(self) -> None: try: if self.socket is not None: self.socket.close(linger=0) @@ -115,7 +122,7 @@ def _reset_connection(self): if not self._closed: self.connect() - def _handle_server_error(self, err): + def _handle_server_error(self, err: Any) -> None: if isinstance(err, str): logger.error(err) if self.raise_exceptions: @@ -132,7 +139,7 @@ def _handle_server_error(self, err): raise TypeError(msg) logger.error(msg) - def disconnect(self): + def disconnect(self) -> None: self._closed = True if self.socket is not None: try: @@ -149,7 +156,9 @@ def disconnect(self): self.connected = False -def sendRequest(message, host="localhost", port=DEFAULT_PORT): +def sendRequest( + message: Any, host: str = "localhost", port: int = DEFAULT_PORT +) -> Any: with BaseClient(host, port) as cli: ret = cli.ask(message) return ret diff --git a/src/instrumentserver/client/proxy.py b/src/instrumentserver/client/proxy.py index a574c68..0262397 100644 --- a/src/instrumentserver/client/proxy.py +++ b/src/instrumentserver/client/proxy.py @@ -13,7 +13,7 @@ import threading from contextlib import contextmanager from types import MethodType -from typing import Any, Dict, List, Optional, Union +from typing import Any, Callable, Dict, List, Optional, Union import qcodes as qc import zmq @@ -52,7 +52,7 @@ class ProxyMixin: def __init__( self, - *args, + *args: Any, cli: Optional["Client"] = None, host: Optional[str] = "localhost", port: Optional[int] = DEFAULT_PORT, @@ -60,8 +60,8 @@ def __init__( bluePrint: Optional[ Union[ParameterBluePrint, InstrumentModuleBluePrint, MethodBluePrint] ] = None, - **kwargs, - ): + **kwargs: Any, + ) -> None: self.cli = cli self.host = host @@ -83,20 +83,20 @@ def __init__( super().__init__(*args, **kwargs) self.__doc__ = self.bp.docstring - def initKwargsFromBluePrint(self, bp): + def initKwargsFromBluePrint(self, bp: Any) -> Dict[str, Any]: raise NotImplementedError - def askServer(self, message: ServerInstruction): + def askServer(self, message: ServerInstruction) -> Any: if self.cli is not None: return self.cli.ask(message) elif self.host is not None and self.port is not None: return sendRequest(message, self.host, self.port) - def _getBluePrintFromServer(self, path): + def _getBluePrintFromServer(self, path: str) -> Any: req = ServerInstruction(operation=Operation.get_blueprint, requested_path=path) return self.askServer(req) - def get_snapshot(self, *args, **kwargs): + def get_snapshot(self, *args: Any, **kwargs: Any) -> Any: req = ServerInstruction( operation=Operation.call, call_spec=CallSpec( @@ -122,15 +122,15 @@ class ProxyParameter(ProxyMixin, Parameter): def __init__( self, name: str, - *args, + *args: Any, cli: Optional["Client"] = None, host: Optional[str] = "localhost", port: Optional[int] = DEFAULT_PORT, remotePath: Optional[str] = None, bluePrint: Optional[ParameterBluePrint] = None, setpoints_instrument: Optional[Instrument] = None, - **kwargs, - ): + **kwargs: Any, + ) -> None: super().__init__( name, @@ -151,8 +151,8 @@ def __init__( ] setattr(self, "setpoints", setpoints) - def initKwargsFromBluePrint(self, bp): - kwargs = {} + def initKwargsFromBluePrint(self, bp: Any) -> Dict[str, Any]: + kwargs: Dict[str, Any] = {} if bp.settable: kwargs["set_cmd"] = self._remoteSet else: @@ -167,14 +167,14 @@ def initKwargsFromBluePrint(self, bp): kwargs["docstring"] = bp.docstring return kwargs - def _remoteSet(self, value: Any): + def _remoteSet(self, value: Any) -> Any: msg = ServerInstruction( operation=Operation.call, call_spec=CallSpec(target=self.remotePath, args=(value,)), ) return self.askServer(msg) - def _remoteGet(self): + def _remoteGet(self) -> Any: msg = ServerInstruction( operation=Operation.call, call_spec=CallSpec( @@ -197,14 +197,14 @@ class ProxyInstrumentModule(ProxyMixin, InstrumentBase): def __init__( self, name: str, - *args, + *args: Any, cli: Optional["Client"] = None, host: Optional[str] = "localhost", port: Optional[int] = DEFAULT_PORT, remotePath: Optional[str] = None, bluePrint: Optional[InstrumentModuleBluePrint] = None, - **kwargs, - ): + **kwargs: Any, + ) -> None: super().__init__( name, @@ -220,16 +220,16 @@ def __init__( # FIXME: This is not consistent with how mixin handles a `None` client. However, this seems like a more # elegant solution than any time we need the client to check to be None, start a new context client instead. if cli is None: - self.cli = Client(host=host, port=port) + self.cli = Client(host=host, port=port) # type: ignore[arg-type] for mn in self.bp.methods.keys(): if mn == "remove_parameter": - def remove_parameter(obj, name: str): + def remove_parameter(obj: Any, name: str) -> None: obj.cli.call(f"{obj.remotePath}.remove_parameter", name) obj.update() - self.remove_parameter = MethodType(remove_parameter, self) + self.remove_parameter = MethodType(remove_parameter, self) # type: ignore[method-assign] self.parameters.pop("IDN", None) # we will redefine this later @@ -242,7 +242,7 @@ def remove_parameter(obj, name: str): self.update() @contextmanager - def _updating(self): + def _updating(self) -> Any: old = self.is_updating self.is_updating = True try: @@ -250,17 +250,17 @@ def _updating(self): finally: self.is_updating = old - def initKwargsFromBluePrint(self, bp): + def initKwargsFromBluePrint(self, bp: Any) -> Dict[str, Any]: return {} - def update(self): - self.cli.invalidateBlueprint(self.remotePath) - self.bp = self.cli.getBluePrint(self.remotePath) + def update(self) -> None: + self.cli.invalidateBlueprint(self.remotePath) # type: ignore[union-attr] + self.bp = self.cli.getBluePrint(self.remotePath) # type: ignore[union-attr] self._getProxyParameters() self._getProxyMethods() self._getProxySubmodules() - def set_parameters(self, **param_dict: dict): + def set_parameters(self, **param_dict: Any) -> None: """ Set instrument parameters in batch with a dict, keyed by parameter names. @@ -273,7 +273,7 @@ def set_parameters(self, **param_dict: dict): f"{self.bp.instrument_module_class} instrument does not have parameter '{k}'" ) - def add_parameter(self, name: str, *arg, **kw): + def add_parameter(self, name: str, *arg: Any, **kw: Any) -> None: # type: ignore[override] """Add a parameter to the proxy instrument. If a parameter of that name already exists in the server-side instrument, @@ -290,7 +290,7 @@ def add_parameter(self, name: str, *arg, **kw): self.cli.call(self.name + ".add_parameter", name, *arg, **kw) self.update() - def remove_parameter(self, name: str, *arg, **kw): + def remove_parameter(self, name: str, *arg: Any, **kw: Any) -> None: """Removes parameter from the proxy instrument. Checking whether the paremeter exists or not is left to the instrument in the server. This is to avoid having @@ -333,7 +333,7 @@ def _getProxyParameters(self) -> None: for k in delKeys: del self.parameters[k] - def _getProxyMethods(self): + def _getProxyMethods(self) -> None: """Based on the method blue print replied from server, add the instrument functions to the proxy instrument class. """ @@ -344,8 +344,8 @@ def _getProxyMethods(self): setattr(self, n, MethodType(fun, self)) self.functions[n] = getattr(self, n) - def _makeProxyMethod(self, bp: MethodBluePrint): - def wrap(*a, **k): + def _makeProxyMethod(self, bp: MethodBluePrint) -> Callable: + def wrap(*a: Any, **k: Any) -> Any: msg = ServerInstruction( operation=Operation.call, call_spec=CallSpec(target=bp.path, args=a, kwargs=k), @@ -379,12 +379,12 @@ def wrap(*a, **k): # make sure the method knows the wrap function. # TODO: this is not complete! globs = {"wrap": wrap, "qcodes": qc, "collections": collections} - _ret = exec(new_func_str, globs) + exec(new_func_str, globs) fun = globs[bp.name] fun.__doc__ = bp.docstring - return globs[bp.name] + return globs[bp.name] # type: ignore[return-value] - def _getProxySubmodules(self): + def _getProxySubmodules(self) -> None: """Based on the submodule blue print replied from server, add the proxy submodules to the proxy module class. """ @@ -393,7 +393,7 @@ def _getProxySubmodules(self): submodule = ProxyInstrumentModule( s.name, cli=self.cli, host=self.host, port=self.port, bluePrint=s ) - self.add_submodule(sn, submodule) + self.add_submodule(sn, submodule) # type: ignore[type-var] else: self.submodules[sn].update() @@ -404,7 +404,7 @@ def _getProxySubmodules(self): for k in delKeys: del self.submodules[k] - def _refreshProxySubmodules(self): + def _refreshProxySubmodules(self) -> None: delKeys = [] for sn, s in self.submodules.items(): if sn in self.bp.submodules: @@ -417,15 +417,15 @@ def _refreshProxySubmodules(self): submodule = ProxyInstrumentModule( s.name, cli=self.cli, host=self.host, port=self.port, bluePrint=s ) - self.add_submodule(sn, submodule) + self.add_submodule(sn, submodule) # type: ignore[type-var] else: self.submodules[sn].update() - def __getattr__(self, item): + def __getattr__(self, item: str) -> Any: try: return super().__getattr__(item) except Exception as e: - current_bp = self.cli.getBluePrint(self.remotePath) + current_bp = self.cli.getBluePrint(self.remotePath) # type: ignore[union-attr] if not self.is_updating: if item in current_bp.parameters and item not in self.parameters: self.bp = current_bp @@ -449,14 +449,14 @@ class Client(BaseClient): def __init__( self, - host="localhost", - port=DEFAULT_PORT, - connect=True, - timeout=20, - raise_exceptions=True, - ): + host: str = "localhost", + port: int = DEFAULT_PORT, + connect: bool = True, + timeout: float = 20, + raise_exceptions: bool = True, + ) -> None: super().__init__(host, port, connect, timeout, raise_exceptions) - self._bp_cache = {} + self._bp_cache: Dict[str, Any] = {} self._bp_cache_lock = threading.Lock() def list_instruments(self) -> Dict[str, str]: @@ -513,10 +513,10 @@ def find_or_create_instrument( _ = self.ask(req) return ProxyInstrumentModule(name=name, cli=self, remotePath=name) - def close_instrument(self, instrument_name: str): + def close_instrument(self, instrument_name: str) -> Any: self.call("close_and_remove_instrument", instrument_name) - def call(self, target, *args, **kwargs): + def call(self, target: str, *args: Any, **kwargs: Any) -> Any: msg = ServerInstruction( operation=Operation.call, call_spec=CallSpec( @@ -527,10 +527,10 @@ def call(self, target, *args, **kwargs): ) return self.ask(msg) - def get_instrument(self, name): + def get_instrument(self, name: str) -> ProxyInstrumentModule: return ProxyInstrumentModule(name=name, cli=self, remotePath=name) - def getBluePrint(self, path): + def getBluePrint(self, path: str) -> Any: """ get blueprint from server :param path: @@ -550,7 +550,7 @@ def getBluePrint(self, path): self._bp_cache[path] = bp return bp - def invalidateBlueprint(self, path=None): + def invalidateBlueprint(self, path: Optional[str] = None) -> None: """ invalidate a parameter in the blueprint cache :param path: @@ -564,7 +564,9 @@ def invalidateBlueprint(self, path=None): if k == path or k.startswith(path + "."): del self._bp_cache[k] - def get_snapshot(self, instrument: str | None = None, *args, **kwargs): + def get_snapshot( + self, instrument: str | None = None, *args: Any, **kwargs: Any + ) -> Any: msg = ServerInstruction( operation=Operation.call, call_spec=CallSpec( @@ -579,9 +581,9 @@ def getParamDict( self, instrument: str | None = None, attrs: List[str] = ["value"], - *args, - **kwargs, - ): + *args: Any, + **kwargs: Any, + ) -> Any: msg = ServerInstruction( operation=Operation.get_param_dict, serialization_opts=ParameterSerializeSpec( @@ -594,8 +596,12 @@ def getParamDict( return self.ask(msg) def paramsToFile( - self, filePath: str, instruments: Optional[List[str]] = None, *args, **kwargs - ): + self, + filePath: str, + instruments: Optional[List[str]] = None, + *args: Any, + **kwargs: Any, + ) -> None: filePath = os.path.abspath(filePath) folder, file = os.path.split(filePath) @@ -605,7 +611,7 @@ def paramsToFile( else: params = {} for instrument_name in instruments: - inst_params = self.getParamDict( + inst_params = self.getParamDict( # type: ignore[misc] instrument=instrument_name, *args, **kwargs ) params.update(inst_params) @@ -618,14 +624,16 @@ def paramsToFile( with open(filePath, "w") as f: json.dump(params, f, indent=2, sort_keys=True) - def setParameters(self, parameters: Dict[str, Any]): + def setParameters(self, parameters: Dict[str, Any]) -> Any: msg = ServerInstruction( operation=Operation.set_params, set_parameters=parameters, ) return self.ask(msg) - def paramsFromFile(self, filePath: str, instruments: Optional[List[str]] = None): + def paramsFromFile( + self, filePath: str, instruments: Optional[List[str]] = None + ) -> None: params = None if os.path.exists(filePath): with open(filePath, "r") as f: @@ -692,11 +700,11 @@ def __init__( self.connected = False self._stop = False - self._ctx = None - self._sock = None + self._ctx: Optional[zmq.Context] = None + self._sock: Optional[zmq.Socket] = None @QtCore.Slot() - def connect(self): + def connect(self) -> bool: """ Connects the subscription client with the broadcast and runs an infinite loop to check for updates. @@ -743,14 +751,14 @@ def connect(self): return True @QtCore.Slot() - def stop(self): + def stop(self) -> None: """ Stops the listener gracefully. """ self._stop = True self.connected = False - def disconnect(self): + def disconnect(self) -> None: """ Alias for stop() for backwards compatibility. """ @@ -758,25 +766,27 @@ def disconnect(self): class _QtAdapter(QtCore.QObject): - def __init__(self, parent, *arg, **kw): + def __init__( + self, parent: Optional[QtCore.QObject], *arg: Any, **kw: Any + ) -> None: super().__init__(parent) class QtClient(_QtAdapter, Client): def __init__( self, - parent=None, - host="localhost", - port=DEFAULT_PORT, - connect=True, - timeout=5, - raise_exceptions=True, - ): + parent: Optional[QtCore.QObject] = None, + host: str = "localhost", + port: int = DEFAULT_PORT, + connect: bool = True, + timeout: float = 5, + raise_exceptions: bool = True, + ) -> None: # Calling the parents like this ensures that the arguments arrive to the parents properly. _QtAdapter.__init__(self, parent=parent) Client.__init__(self, host, port, connect, timeout, raise_exceptions) - def disconnect(self, *args, **kwargs): + def disconnect(self, *args: Any, **kwargs: Any) -> Any: # QObject.disconnect() shadows BaseClient.disconnect() via MRO, so # explicitly dispatch to BaseClient.disconnect() when called without # Qt signal arguments. Preserve Qt's signal-disconnect semantics when @@ -789,14 +799,14 @@ def disconnect(self, *args, **kwargs): class ClientStation: def __init__( self, - host="localhost", - port=DEFAULT_PORT, - connect=True, - timeout=20, - raise_exceptions=True, - config_path: str = None, - param_path: str = None, - ): + host: str = "localhost", + port: int = DEFAULT_PORT, + connect: bool = True, + timeout: float = 20, + raise_exceptions: bool = True, + config_path: Optional[str] = None, + param_path: Optional[str] = None, + ) -> None: """ A lightweight container for managing a collection of proxy instruments on the client side. @@ -860,7 +870,7 @@ def __init__( self._create_instruments(instrument_config) self._config_path = config_path - def _make_client(self, connect=True): + def _make_client(self, connect: bool = True) -> Client: cli = Client( host=self._host, port=self._port, @@ -870,7 +880,7 @@ def _make_client(self, connect=True): ) return cli - def _create_instruments(self, instrument_dict: dict): + def _create_instruments(self, instrument_dict: dict) -> None: """ Create proxy instruments based on the parameters in instrument_dict. Uses 'type' field from server config format. @@ -889,25 +899,25 @@ def _create_instruments(self, instrument_dict: dict): ) self.instruments[name] = instrument - def close_instrument(self, instrument_name: str): + def close_instrument(self, instrument_name: str) -> Any: self.client.close_instrument(instrument_name) - def disconnect(self): + def disconnect(self) -> None: """Tear down the underlying client and release its zmq resources.""" if self.client is not None: try: self.client.disconnect() except Exception: pass - self.client = None + self.client = None # type: ignore[assignment] @staticmethod - def _remake_client_station_when_fail(func): + def _remake_client_station_when_fail(func: Callable) -> Callable: """ Decorator for remaking a client station object when function call fails """ - def wrapper(self, *args, **kwargs): + def wrapper(self: "ClientStation", *args: Any, **kwargs: Any) -> Any: try: retval = func(self, *args, **kwargs) except Exception as e: @@ -951,7 +961,7 @@ def get_instrument(self, name: str) -> ProxyInstrument: return self.instruments[name] @_remake_client_station_when_fail - def get_parameters(self, instruments: List[str] = None) -> Dict: + def get_parameters(self, instruments: Optional[List[str]] = None) -> Dict: """ Get all instrument parameters as a nested dictionary. @@ -959,9 +969,9 @@ def get_parameters(self, instruments: List[str] = None) -> Dict: :param instruments: list of instrument names. If None, all instrument parameters are returned. :return: """ - inst_params = {} + inst_params: Dict[str, Any] = {} if instruments is None: - instruments = self.instruments.keys() + instruments = list(self.instruments.keys()) for name in instruments: ins_paras = self.client.getParamDict(name, get=True) ins_paras = flat_to_nested_dict(ins_paras) @@ -970,7 +980,7 @@ def get_parameters(self, instruments: List[str] = None) -> Dict: return inst_params @_remake_client_station_when_fail - def set_parameters(self, inst_params: Dict): + def set_parameters(self, inst_params: Dict) -> None: """ load instrument parameters from a nested dictionary. @@ -997,8 +1007,10 @@ def set_parameters(self, inst_params: Dict): @_remake_client_station_when_fail def save_parameters( - self, file_path: str = None, select_instruments: List[str] = None - ): + self, + file_path: Optional[str] = None, + select_instruments: Optional[List[str]] = None, + ) -> None: """ Save instrument parameters to a JSON file in nested format. @@ -1012,10 +1024,12 @@ def save_parameters( else list(self.instruments.keys()) ) # Delegate to client's paramsToFile - self.client.paramsToFile(file_path, instruments=instruments) + self.client.paramsToFile(file_path, instruments=instruments) # type: ignore[arg-type] @_remake_client_station_when_fail - def load_parameters(self, file_path: str, select_instruments: List[str] = None): + def load_parameters( + self, file_path: str, select_instruments: Optional[List[str]] = None + ) -> None: """ Load instrument parameters from a JSON file. @@ -1031,5 +1045,5 @@ def load_parameters(self, file_path: str, select_instruments: List[str] = None): ) self.client.paramsFromFile(file_path, instruments=instruments) - def __getitem__(self, item): + def __getitem__(self, item: str) -> ProxyInstrument: return self.instruments[item] diff --git a/src/instrumentserver/config.py b/src/instrumentserver/config.py index cc18012..d177c87 100644 --- a/src/instrumentserver/config.py +++ b/src/instrumentserver/config.py @@ -9,7 +9,7 @@ from pathlib import Path from typing import IO -import ruamel.yaml # type: ignore[import-untyped] # Known bugfix under no-fix status: https://sourceforge.net/p/ruamel-yaml/tickets/328/ +import ruamel.yaml # Centralised point of extra fields for the server with its default as value SERVERFIELDS = {"initialize": True} diff --git a/src/instrumentserver/gui/__init__.py b/src/instrumentserver/gui/__init__.py index 664b446..4602f95 100644 --- a/src/instrumentserver/gui/__init__.py +++ b/src/instrumentserver/gui/__init__.py @@ -1,20 +1,24 @@ +from typing import Optional + from .. import QtCore, QtWidgets +from .. import resource # noqa: F401 -def getStyleSheet(): +def getStyleSheet() -> Optional[str]: f = QtCore.QFile(":/style.css") - if f.open(QtCore.QIODevice.ReadOnly | QtCore.QIODevice.Text): + if f.open(QtCore.QIODevice.OpenModeFlag.ReadOnly | QtCore.QIODevice.OpenModeFlag.Text): # type: ignore[call-overload] style = f.readAll() f.close() return str(style, "utf-8") + return None -def widgetDialog(w: QtWidgets.QWidget): +def widgetDialog(w: QtWidgets.QWidget) -> QtWidgets.QDialog: dg = QtWidgets.QDialog() dg.setWindowTitle("instrumentserver") - dg.setWindowFlag(QtCore.Qt.WindowMinimizeButtonHint) - dg.setWindowFlag(QtCore.Qt.WindowMaximizeButtonHint) - dg.widget = w # type: ignore[attr-defined] # I am pretty sure the stubs are wrong for this one. + dg.setWindowFlag(QtCore.Qt.WindowType.WindowMinimizeButtonHint) + dg.setWindowFlag(QtCore.Qt.WindowType.WindowMaximizeButtonHint) + dg.widget = w css = getStyleSheet() w.setStyleSheet(css) @@ -28,7 +32,9 @@ def widgetDialog(w: QtWidgets.QWidget): return dg -def widgetMainWindow(w: QtWidgets.QWidget, name: str = "instrumentserver"): +def widgetMainWindow( + w: QtWidgets.QWidget, name: str = "instrumentserver" +) -> QtWidgets.QMainWindow: mw = QtWidgets.QMainWindow() mw.setWindowTitle(name) mw.setCentralWidget(w) @@ -40,7 +46,7 @@ def widgetMainWindow(w: QtWidgets.QWidget, name: str = "instrumentserver"): return mw -def keepSmallHorizontally(w: QtWidgets.QWidget): +def keepSmallHorizontally(w: QtWidgets.QWidget) -> None: w.setSizePolicy( QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum diff --git a/src/instrumentserver/gui/base_instrument.py b/src/instrumentserver/gui/base_instrument.py index 47bb0e2..9738ee4 100644 --- a/src/instrumentserver/gui/base_instrument.py +++ b/src/instrumentserver/gui/base_instrument.py @@ -104,7 +104,7 @@ import fnmatch from pprint import pprint -from typing import Dict, List, Optional +from typing import Any, Dict, List, Optional from instrumentserver import QtCore, QtGui, QtWidgets @@ -123,12 +123,12 @@ class ItemBase(QtGui.QStandardItem): def __init__( self, - name, - star=False, - trash=False, - showDelegate=True, - element=None, - ): + name: str, + star: bool = False, + trash: bool = False, + showDelegate: bool = True, + element: Any = None, + ) -> None: super().__init__() self.name = name @@ -146,11 +146,11 @@ class DelegateBase(QtWidgets.QStyledItemDelegate): """ @classmethod - def getItem(cls, QModelIndex): + def getItem(cls, QModelIndex: QtCore.QModelIndex) -> QtGui.QStandardItem: proxyModel = QModelIndex.model() - model = proxyModel.sourceModel() - item = model.itemFromIndex(proxyModel.mapToSource(QModelIndex)) + model = proxyModel.sourceModel() # type: ignore[union-attr] + item = model.itemFromIndex(proxyModel.mapToSource(QModelIndex)) # type: ignore[union-attr] if item.column != 0: parent = item.parent() row = item.row() @@ -194,14 +194,14 @@ class InstrumentModelBase(QtGui.QStandardItemModel): def __init__( self, - instrument, + instrument: Any, attr: str, itemClass: type[ItemBase] = ItemBase, itemsStar: Optional[List[str]] = [], itemsTrash: Optional[List[str]] = [], itemsHide: Optional[List[str]] = [], parent: Optional[QtCore.QObject] = None, - ): + ) -> None: super().__init__(parent=parent) @@ -242,7 +242,7 @@ def _matches_any_pattern(name: str, patterns: List[str]) -> bool: return True return False - def loadItems(self, module=None, prefix=None): + def loadItems(self, module: Any = None, prefix: Optional[str] = None) -> None: """ The argument for either submodules or the instrument itself. @@ -258,13 +258,13 @@ def loadItems(self, module=None, prefix=None): # constructor if prefix is not None: objectName = ".".join([prefix, objectName]) - if not self._matches_any_pattern(objectName, self.itemsHide): + if not self._matches_any_pattern(objectName, self.itemsHide): # type: ignore[arg-type] item = self.addItem( fullName=objectName, star=False, trash=False, element=obj ) - if self._matches_any_pattern(objectName, self.itemsTrash): + if self._matches_any_pattern(objectName, self.itemsTrash): # type: ignore[arg-type] self.onItemTrashToggle(item) - if self._matches_any_pattern(objectName, self.itemsStar): + if self._matches_any_pattern(objectName, self.itemsStar): # type: ignore[arg-type] self.onItemStarToggle(item) for submodName, submod in module.submodules.items(): @@ -272,7 +272,7 @@ def loadItems(self, module=None, prefix=None): submodName = ".".join([prefix, submodName]) self.loadItems(submod, submodName) - def refreshAll(self): + def refreshAll(self) -> None: """ Removes all the rows from the model, updates the instrument and loads the model again. """ @@ -281,7 +281,9 @@ def refreshAll(self): self.loadItems() self.modelRefreshed.emit() - def insertItemTo(self, parent, item): + def insertItemTo( + self, parent: QtGui.QStandardItem, item: QtGui.QStandardItem + ) -> None: """ This is the only function that actually inserts items into the model. Overload for models that utilize more columns. **Don't call directly** @@ -293,7 +295,7 @@ def insertItemTo(self, parent, item): else: parent.appendRow(item) - def addItem(self, fullName, **kwargs): + def addItem(self, fullName: str, **kwargs: Any) -> "ItemBase": """ Adds an item to the model. The *args and **kwargs are whatever the specific item needs for a new item. @@ -310,7 +312,7 @@ def addItem(self, fullName, **kwargs): smName = smName + f".{sm}" items = self.findItems( - smName, QtCore.Qt.MatchExactly | QtCore.Qt.MatchRecursive, 0 + smName, QtCore.Qt.MatchFlag.MatchExactly | QtCore.Qt.MatchFlag.MatchRecursive, 0 # type: ignore[arg-type] ) if len(items) == 0: @@ -323,26 +325,26 @@ def addItem(self, fullName, **kwargs): ) # submodules get directly added here and not in the load function, so need to have it here too. if self.loadingItems: - if not self._matches_any_pattern(smName, self.itemsHide): - self.insertItemTo(parent, subModItem) - if self._matches_any_pattern(smName, self.itemsTrash): + if not self._matches_any_pattern(smName, self.itemsHide): # type: ignore[arg-type] + self.insertItemTo(parent, subModItem) # type: ignore[arg-type] + if self._matches_any_pattern(smName, self.itemsTrash): # type: ignore[arg-type] self.onItemTrashToggle(subModItem) - if self._matches_any_pattern(smName, self.itemsStar): + if self._matches_any_pattern(smName, self.itemsStar): # type: ignore[arg-type] self.onItemStarToggle(subModItem) else: - self.insertItemTo(parent, subModItem) - parent = subModItem + self.insertItemTo(parent, subModItem) # type: ignore[arg-type] + parent = subModItem # type: ignore[assignment] else: - parent = items[0] + parent = items[0] # type: ignore[assignment] newItem = self.itemClass(name=fullName, **kwargs) - self.insertItemTo(parent, newItem) + self.insertItemTo(parent, newItem) # type: ignore[arg-type] return newItem - def removeItem(self, fullName): + def removeItem(self, fullName: str) -> None: items = self.findItems( - fullName, QtCore.Qt.MatchExactly | QtCore.Qt.MatchRecursive, 0 + fullName, QtCore.Qt.MatchFlag.MatchExactly | QtCore.Qt.MatchFlag.MatchRecursive, 0 # type: ignore[arg-type] ) if len(items) > 0: @@ -356,7 +358,7 @@ def removeItem(self, fullName): self.removeRow(item.row()) @QtCore.Slot(ItemBase) - def onItemStarToggle(self, item): + def onItemStarToggle(self, item: "ItemBase") -> None: assert isinstance(item, ItemBase) if item.star: item.star = False @@ -367,7 +369,7 @@ def onItemStarToggle(self, item): item.setIcon(QtGui.QIcon(":/icons/star.svg")) @QtCore.Slot(ItemBase) - def onItemTrashToggle(self, item): + def onItemTrashToggle(self, item: "ItemBase") -> None: assert isinstance(item, ItemBase) if item.trash: item.trash = False @@ -399,29 +401,31 @@ def __init__( self.star = False self.trash = False - self.sort(0, QtCore.Qt.DescendingOrder) + self.sort(0, QtCore.Qt.SortOrder.DescendingOrder) @QtCore.Slot(int, QtCore.Qt.SortOrder) - def onSortingIndicatorChanged(self, index, sortingOrder): + def onSortingIndicatorChanged( + self, index: int, sortingOrder: QtCore.Qt.SortOrder + ) -> None: self.sort(index, sortingOrder) @QtCore.Slot() - def onToggleStar(self): + def onToggleStar(self) -> None: if self.star: self.star = False else: self.star = True # When the start status changes, trigger a sorting so that the star items move. - if self.sortOrder() == QtCore.Qt.DescendingOrder: - self.sort(0, QtCore.Qt.AscendingOrder) - self.sort(0, QtCore.Qt.DescendingOrder) - elif self.sortOrder() == QtCore.Qt.AscendingOrder: - self.sort(0, QtCore.Qt.DescendingOrder) - self.sort(0, QtCore.Qt.AscendingOrder) + if self.sortOrder() == QtCore.Qt.SortOrder.DescendingOrder: + self.sort(0, QtCore.Qt.SortOrder.AscendingOrder) + self.sort(0, QtCore.Qt.SortOrder.DescendingOrder) + elif self.sortOrder() == QtCore.Qt.SortOrder.AscendingOrder: + self.sort(0, QtCore.Qt.SortOrder.DescendingOrder) + self.sort(0, QtCore.Qt.SortOrder.AscendingOrder) @QtCore.Slot() - def onToggleTrash(self): + def onToggleTrash(self) -> None: if self.trash: self.trash = False else: @@ -429,17 +433,17 @@ def onToggleTrash(self): self.triggerFiltering() @QtCore.Slot(str) - def onTextFilterChange(self, filter: str): + def onTextFilterChange(self, filter: str) -> None: self.filterIncoming.emit() self.setFilterRegExp(filter) self.filterFinished.emit() - def triggerFiltering(self): + def triggerFiltering(self) -> None: self.filterIncoming.emit() self.invalidateFilter() self.filterFinished.emit() - def _isParentTrash(self, parent): + def _isParentTrash(self, parent: Optional["ItemBase"]) -> bool: """ Recursive function to see if any parent of an item is trash. """ @@ -449,7 +453,7 @@ def _isParentTrash(self, parent): if parent.trash: return True - return self._isParentTrash(parent.parent()) + return self._isParentTrash(parent.parent()) # type: ignore[arg-type] def filterAcceptsRow( self, source_row: int, source_parent: QtCore.QModelIndex @@ -472,7 +476,7 @@ def filterAcceptsRow( # Assertion is there to satisfy mypy. item can be None, that is why we check before making the assertion if item is not None: assert isinstance(item, ItemBase) - if self._isParentTrash(parent) or getattr( + if self._isParentTrash(parent) or getattr( # type: ignore[arg-type] item, "trash", False ): # item could be None when it's trashed and hidden return False @@ -485,7 +489,7 @@ def lessThan(self, left: QtCore.QModelIndex, right: QtCore.QModelIndex) -> bool: """ # The order in which things get constructed seems to impact this. - # When the application is first starting, the proxy model does not have the star attribute. + # When the application is first starting, the proxy model does not have the star attribute. if hasattr(self, "star"): if self.star: model = self.sourceModel() @@ -493,16 +497,16 @@ def lessThan(self, left: QtCore.QModelIndex, right: QtCore.QModelIndex) -> bool: leftItem = model.itemFromIndex(left) rightItem = model.itemFromIndex(right) if hasattr(leftItem, "star") and hasattr(rightItem, "star"): - if self.sortOrder() == QtCore.Qt.DescendingOrder: - if rightItem.star and not leftItem.star: + if self.sortOrder() == QtCore.Qt.SortOrder.DescendingOrder: + if rightItem.star and not leftItem.star: # type: ignore[union-attr] return True - elif not rightItem.star and leftItem.star: + elif not rightItem.star and leftItem.star: # type: ignore[union-attr] return False - elif self.sortOrder() == QtCore.Qt.AscendingOrder: - if rightItem.star and not leftItem.star: + elif self.sortOrder() == QtCore.Qt.SortOrder.AscendingOrder: + if rightItem.star and not leftItem.star: # type: ignore[union-attr] return False - elif not rightItem.star and leftItem.star: + elif not rightItem.star and leftItem.star: # type: ignore[union-attr] return True return super().lessThan(left, right) @@ -519,10 +523,10 @@ class InstrumentTreeViewBase(QtWidgets.QTreeView): def __init__( self, - model, + model: QtCore.QAbstractItemModel, delegateColumns: Optional[List[int]] = None, parent: Optional[QtWidgets.QWidget] = None, - ): + ) -> None: super().__init__(parent=parent) # Indicates if a column is using delegates. @@ -547,8 +551,8 @@ def __init__( self.setSortingEnabled(False) # The tree should not have anything to do with filtering itself since that is left for the proxy model. - self.header().setSortIndicatorShown(True) - self.header().setSectionsClickable(True) + self.header().setSortIndicatorShown(True) # type: ignore[union-attr] + self.header().setSectionsClickable(True) # type: ignore[union-attr] self.setAlternatingRowColors(True) @@ -566,11 +570,11 @@ def __init__( self.contextMenu.addAction(self.starItemAction) self.contextMenu.addAction(self.trashItemAction) - self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu) self.customContextMenuRequested.connect(self.onContextMenuRequested) @QtCore.Slot() - def fillCollapsedDict(self, parentItem: Optional[ItemBase] = None): + def fillCollapsedDict(self, parentItem: Optional[ItemBase] = None) -> None: """ Fills the collapsed state dictionary to be recovered after a filter event occured. """ @@ -585,8 +589,8 @@ def fillCollapsedDict(self, parentItem: Optional[ItemBase] = None): proxyIndex = m.mapFromSource(index) if proxyIndex.isValid(): self.collapsedState[persistentIndex] = self.isExpanded(proxyIndex) - if item.hasChildren(): - self.fillCollapsedDict(item) + if item.hasChildren(): # type: ignore[union-attr] + self.fillCollapsedDict(item) # type: ignore[arg-type] else: for i in range(parentItem.rowCount()): child = parentItem.child(i, 0) @@ -597,53 +601,55 @@ def fillCollapsedDict(self, parentItem: Optional[ItemBase] = None): proxyIndex = m.mapFromSource(childIndex) if proxyIndex.isValid(): self.collapsedState[persistentIndex] = self.isExpanded(proxyIndex) - if child.hasChildren(): - self.fillCollapsedDict(child) + if child.hasChildren(): # type: ignore[union-attr] + self.fillCollapsedDict(child) # type: ignore[arg-type] @QtCore.Slot() - def restoreCollapsedDict(self): + def restoreCollapsedDict(self) -> None: """ Goes through the collapsed state dictionary, and expands any item that should be expanded. It also resets the persistent editors and triggers a resizing of delegates. """ for persistentIndex, state in self.collapsedState.items(): - modelIndex = self.modelActual.index( + modelIndex = self.modelActual.index( # type: ignore[union-attr] persistentIndex.row(), persistentIndex.column(), persistentIndex.parent(), ) - item = self.modelActual.itemFromIndex(modelIndex) - proxyIndex = self.model().mapFromSource(modelIndex) + item = self.modelActual.itemFromIndex(modelIndex) # type: ignore[union-attr] + proxyIndex = self.model().mapFromSource(modelIndex) # type: ignore[union-attr] self.setExpanded(proxyIndex, state) if item.showDelegate: delegateIndexes = [ - self.modelActual.index( + self.modelActual.index( # type: ignore[union-attr] persistentIndex.row(), x, persistentIndex.parent() ) - for x in self.delegateColumns + for x in self.delegateColumns # type: ignore[union-attr] ] proxyDelegateIndexes = [ - self.model().mapFromSource(index) for index in delegateIndexes + self.model().mapFromSource(index) for index in delegateIndexes # type: ignore[union-attr] ] for delegateIndex in proxyDelegateIndexes: self.openPersistentEditor(delegateIndex) self.scheduleDelayedItemsLayout() - def setAllDelegatesPersistent(self, parentIndex=None): + def setAllDelegatesPersistent( + self, parentIndex: Optional[QtCore.QModelIndex] = None + ) -> None: """ Recursive function that goes through the entire model and sets all delegates to be persistent editors :param parentIndex: If None, start the process. if it's an item, it will go through the children """ if parentIndex is None: - for i in range(self.model().rowCount()): - for column in self.delegateColumns: - index = self.model().index(i, column) - index0 = self.model().index( + for i in range(self.model().rowCount()): # type: ignore[union-attr] + for column in self.delegateColumns: # type: ignore[union-attr] + index = self.model().index(i, column) # type: ignore[union-attr] + index0 = self.model().index( # type: ignore[union-attr] i, 0 ) # Only items at column 0 hold children and model info - item0 = self.modelActual.itemFromIndex( - self.model().mapToSource(index0) + item0 = self.modelActual.itemFromIndex( # type: ignore[union-attr] + self.model().mapToSource(index0) # type: ignore[union-attr] ) if item0.showDelegate: self.openPersistentEditor(index) @@ -651,18 +657,18 @@ def setAllDelegatesPersistent(self, parentIndex=None): self.setAllDelegatesPersistent(index0) else: - parentItem = self.modelActual.itemFromIndex( - self.model().mapToSource(parentIndex) + parentItem = self.modelActual.itemFromIndex( # type: ignore[union-attr] + self.model().mapToSource(parentIndex) # type: ignore[union-attr] ) for i in range(parentItem.rowCount()): - for column in self.delegateColumns: + for column in self.delegateColumns: # type: ignore[union-attr] item = parentItem.child(i, column) item0 = parentItem.child(i, 0) - index = self.model().mapFromSource( - self.modelActual.indexFromItem(item) + index = self.model().mapFromSource( # type: ignore[union-attr] + self.modelActual.indexFromItem(item) # type: ignore[union-attr] ) - index0 = self.model().mapFromSource( - self.modelActual.indexFromItem(item0) + index0 = self.model().mapFromSource( # type: ignore[union-attr] + self.modelActual.indexFromItem(item0) # type: ignore[union-attr] ) if item0.showDelegate: self.openPersistentEditor(index) @@ -670,7 +676,7 @@ def setAllDelegatesPersistent(self, parentIndex=None): self.setAllDelegatesPersistent(index0) @QtCore.Slot(object) - def onCheckDelegate(self, item): + def onCheckDelegate(self, item: Optional["ItemBase"]) -> None: """ Makes sure that the delegates are shown if needed. @@ -680,24 +686,24 @@ def onCheckDelegate(self, item): if item.showDelegate: row = item.row() parent = item.parent() - for column in self.delegateColumns: + for column in self.delegateColumns: # type: ignore[union-attr] if parent is None: - sibling = self.modelActual.item(row, column) + sibling = self.modelActual.item(row, column) # type: ignore[union-attr] else: sibling = parent.child(row, column) - index = self.model().mapFromSource( - self.modelActual.indexFromItem(sibling) + index = self.model().mapFromSource( # type: ignore[union-attr] + self.modelActual.indexFromItem(sibling) # type: ignore[union-attr] ) self.openPersistentEditor(index) self.scheduleDelayedItemsLayout() @QtCore.Slot(QtCore.QPoint) - def onContextMenuRequested(self, pos): + def onContextMenuRequested(self, pos: QtCore.QPoint) -> None: # We get the item from the real model, not the proxy model - originalModel = self.model().sourceModel() + originalModel = self.model().sourceModel() # type: ignore[union-attr] proxyIndex = self.indexAt(pos) - index = self.model().mapToSource(proxyIndex) + index = self.model().mapToSource(proxyIndex) # type: ignore[union-attr] # catch the case if the user rightcliks on any other column if index.column() != 0: @@ -730,11 +736,11 @@ def onContextMenuRequested(self, pos): self.contextMenu.exec_(self.mapToGlobal(pos)) @QtCore.Slot() - def onStarActionTrigger(self): + def onStarActionTrigger(self) -> None: self.itemStarToggle.emit(self.lastSelectedItem) @QtCore.Slot() - def onTrashActionTrigger(self): + def onTrashActionTrigger(self) -> None: self.itemTrashToggle.emit(self.lastSelectedItem) @@ -755,16 +761,16 @@ class InstrumentDisplayBase(QtWidgets.QWidget): def __init__( self, - instrument, + instrument: Any, attr: str, - itemType=ItemBase, - modelType=InstrumentModelBase, - proxyModelType=InstrumentSortFilterProxyModel, - viewType=InstrumentTreeViewBase, + itemType: type = ItemBase, + modelType: type = InstrumentModelBase, + proxyModelType: type = InstrumentSortFilterProxyModel, + viewType: type = InstrumentTreeViewBase, callSignals: bool = True, parent: Optional[QtWidgets.QWidget] = None, - **modelKwargs, - ): + **modelKwargs: Any, + ) -> None: super().__init__(parent=parent) # initializing variables @@ -793,7 +799,7 @@ def __init__( if callSignals: self.connectSignals() - def connectSignals(self): + def connectSignals(self) -> None: """ Connects all the signals to slots of different classes. Override to add more signals """ @@ -812,7 +818,7 @@ def connectSignals(self): self.proxyModel.onSortingIndicatorChanged ) - def makeToolbar(self): + def makeToolbar(self) -> QtWidgets.QToolBar: """ Creates the toolbar, override to add more buttons to the toolbar. """ @@ -823,7 +829,7 @@ def makeToolbar(self): QtGui.QIcon(":/icons/refresh.svg"), "refresh all items from the instrument", ) - refreshAction.triggered.connect(lambda x: self.refreshAll()) + refreshAction.triggered.connect(lambda x: self.refreshAll()) # type: ignore[union-attr] toolbar.addSeparator() @@ -831,27 +837,27 @@ def makeToolbar(self): QtGui.QIcon(":/icons/expand.svg"), "expand tree", ) - expandAction.triggered.connect(lambda x: self.view.expandAll()) + expandAction.triggered.connect(lambda x: self.view.expandAll()) # type: ignore[union-attr] collapseAction = toolbar.addAction( QtGui.QIcon(":/icons/collapse.svg"), "collapse tree", ) - collapseAction.triggered.connect(lambda x: self.view.collapseAll()) + collapseAction.triggered.connect(lambda x: self.view.collapseAll()) # type: ignore[union-attr] toolbar.addSeparator() starAction = toolbar.addAction( QtGui.QIcon(":/icons/star.svg"), "Move Starred items to the top" ) - starAction.setCheckable(True) - starAction.triggered.connect(lambda x: self.promoteStar()) + starAction.setCheckable(True) # type: ignore[union-attr] + starAction.triggered.connect(lambda x: self.promoteStar()) # type: ignore[union-attr] trashAction = toolbar.addAction( QtGui.QIcon(":/icons/trash-crossed.svg"), "Hide trashed items" ) - trashAction.setCheckable(True) - trashAction.triggered.connect(lambda x: self.hideTrash()) + trashAction.setCheckable(True) # type: ignore[union-attr] + trashAction.triggered.connect(lambda x: self.hideTrash()) # type: ignore[union-attr] # Debugging tools keep commented for commits. # printAction = toolbar.addAction( @@ -865,33 +871,33 @@ def makeToolbar(self): return toolbar @QtCore.Slot() - def hideTrash(self): + def hideTrash(self) -> None: self.proxyModel.onToggleTrash() @QtCore.Slot() - def promoteStar(self): + def promoteStar(self) -> None: self.proxyModel.onToggleStar() @QtCore.Slot() - def refreshAll(self): + def refreshAll(self) -> None: self.model.refreshAll() - def debuggingMethod(self): + def debuggingMethod(self) -> None: """ This is just a debugging method. """ - items = {} + items: Dict[str, Any] = {} - def fillChildren(parent): + def fillChildren(parent: QtGui.QStandardItem) -> None: for i in range(parent.rowCount()): item = parent.child(i, 0) - items[item.name] = { + items[item.name] = { # type: ignore[union-attr] "item": item, - "star": item.star, - "trash": item.trash, + "star": item.star, # type: ignore[union-attr] + "trash": item.trash, # type: ignore[union-attr] } - if item.hasChildren(): - fillChildren(item) + if item.hasChildren(): # type: ignore[union-attr] + fillChildren(item) # type: ignore[arg-type] for i in range(self.model.rowCount()): item = self.model.item(i, 0) diff --git a/src/instrumentserver/gui/instruments.py b/src/instrumentserver/gui/instruments.py index 9815a65..a2a0fe8 100644 --- a/src/instrumentserver/gui/instruments.py +++ b/src/instrumentserver/gui/instruments.py @@ -1,6 +1,6 @@ import inspect import logging -from typing import Callable, Dict, Optional, Union +from typing import Any, Callable, Dict, Optional, Union from qcodes import Instrument @@ -43,7 +43,7 @@ class AddParameterWidget(QtWidgets.QWidget): def __init__( self, parent: Optional[QtWidgets.QWidget] = None, typeInput: bool = False - ): + ) -> None: super().__init__(parent) self.typeInput = typeInput @@ -53,19 +53,19 @@ def __init__( self.nameEdit = QtWidgets.QLineEdit(self) lbl = QtWidgets.QLabel("Name:") - lbl.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter) # type: ignore[arg-type] layout.addWidget(lbl, 0, 0) layout.addWidget(self.nameEdit, 0, 1) self.valueEdit = QtWidgets.QLineEdit(self) lbl = QtWidgets.QLabel("Value:") - lbl.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter) # type: ignore[arg-type] layout.addWidget(lbl, 0, 2) layout.addWidget(self.valueEdit, 0, 3) self.unitEdit = QtWidgets.QLineEdit(self) lbl = QtWidgets.QLabel("Unit:") - lbl.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter) # type: ignore[arg-type] layout.addWidget(lbl, 0, 4) layout.addWidget(self.unitEdit, 0, 5) @@ -80,7 +80,7 @@ def __init__( str(parameterTypes[ParameterTypes.numeric]["name"]) ) lbl = QtWidgets.QLabel("Type:") - lbl.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter) # type: ignore[arg-type] layout.addWidget(lbl, 1, 0) layout.addWidget(self.typeSelect, 1, 1) @@ -94,7 +94,7 @@ def __init__( " - 'String': min_length=0, max_length=1e9\n" "See qcodes.utils.validators for details." ) - lbl.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter) # type: ignore[arg-type] layout.addWidget(lbl, 1, 2) layout.addWidget(self.valsArgsEdit, 1, 3) @@ -121,19 +121,19 @@ def __init__( self.invalidParamRequested.connect(self.setError) @QtCore.Slot() - def clear(self): + def clear(self) -> None: self.clearError() self.nameEdit.setText("") self.valueEdit.setText("") self.unitEdit.setText("") if self.typeInput: self.typeSelect.setCurrentText( - parameterTypes[ParameterTypes.numeric]["name"] + parameterTypes[ParameterTypes.numeric]["name"] # type: ignore[arg-type] ) self.valsArgsEdit.setText("") @QtCore.Slot(bool) - def requestNewParameter(self, _): + def requestNewParameter(self, _: bool) -> None: self.clearError() name = self.nameEdit.text().strip() @@ -153,13 +153,13 @@ def requestNewParameter(self, _): self.newParamRequested.emit(name, value, unit, ptype, valsArgs) @QtCore.Slot(str) - def setError(self, message: str): + def setError(self, message: str) -> None: self.addButton.setStyleSheet(""" QPushButton { background-color: red } """) self.addButton.setToolTip(message) - def clearError(self): + def clearError(self) -> None: self.addButton.setStyleSheet("") self.addButton.setToolTip("") @@ -173,7 +173,13 @@ class MethodDisplay(QtWidgets.QWidget): #: emitted when the widget runs a function and is successful. Emits the return value as a string. runSuccessful = QtCore.Signal(str) - def __init__(self, fun, fullName=None, *args, **kwargs): + def __init__( + self, + fun: Callable, + fullName: Optional[str] = None, + *args: Any, + **kwargs: Any, + ) -> None: super().__init__(*args, **kwargs) self.fun = fun @@ -202,7 +208,7 @@ def __init__(self, fun, fullName=None, *args, **kwargs): self._layout.setContentsMargins(1, 1, 1, 1) @QtCore.Slot() - def runFun(self): + def runFun(self) -> None: try: args, kwargs = self.anyInput.value() if kwargs is not None: @@ -220,7 +226,7 @@ def runFun(self): logger.warning(f"'{self.fullName}' Raised the following execution: {e}") @classmethod - def getTooltipFromFun(cls, fun: Callable): + def getTooltipFromFun(cls, fun: Callable) -> str: """ Returns the signature of the function with its documentation underneath. """ @@ -233,7 +239,7 @@ def getTooltipFromFun(cls, fun: Callable): class ItemParameters(ItemBase): - def __init__(self, unit="", **kwargs): + def __init__(self, unit: str = "", **kwargs: Any) -> None: super().__init__(**kwargs) self.unit = unit @@ -244,14 +250,14 @@ class ParameterDelegate(DelegateBase): The delegate for the InstrumentParameters widget. """ - def __init__(self, parent=None): + def __init__(self, parent: Optional[QtCore.QObject] = None) -> None: super().__init__(parent=parent) # Stores as key the name of the item and as value the widget that the delegate creates. # used to keep a reference to the widget. self.parameters: Dict[str, QtWidgets.QWidget] = {} - def createEditor( + def createEditor( # type: ignore[override] self, widget: QtWidgets.QWidget, option: QtWidgets.QStyleOptionViewItem, @@ -261,10 +267,10 @@ def createEditor( This is the function that is supposed to create the widget. It should return it. """ item = self.getItem(index) - element = item.element + element = item.element # type: ignore[attr-defined] ret = ParameterWidget(element, widget) - self.parameters[item.name] = ret + self.parameters[item.name] = ret # type: ignore[attr-defined] # Try to fetch and display current value immediately # ---- Chao: removed because the constructor of ParameterWidget object already calls parameter get ---- # if element.gettable: @@ -281,7 +287,7 @@ class ModelParameters(InstrumentModelBase): # name, second object is its new value itemNewValue = QtCore.Signal(object, object) - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: # make sure we pass the server ip and port properly to the subscriber when the values are not defaults. subClientArgs = { "sub_host": kwargs.pop("sub_host", "localhost"), @@ -297,13 +303,13 @@ def __init__(self, *args, **kwargs): self.subClient = SubClient([self.instrument.name], **subClientArgs) self.subClient.moveToThread(self.cliThread) - self.cliThread.started.connect(self.subClient.connect) + self.cliThread.started.connect(self.subClient.connect) # type: ignore[arg-type] self.subClient.update.connect(self.updateParameter) self.subClient.finished.connect(self.cliThread.quit) self.cliThread.start() - def stopListener(self): + def stopListener(self) -> None: """Stop the background listener thread and wait for it to exit.""" if self.subClient is not None: self.subClient.stop() @@ -312,7 +318,7 @@ def stopListener(self): self.cliThread.wait(3000) @QtCore.Slot(ParameterBroadcastBluePrint) - def updateParameter(self, bp: ParameterBroadcastBluePrint): + def updateParameter(self, bp: ParameterBroadcastBluePrint) -> None: fullName = ".".join(bp.name.split(".")[1:]) if bp.action == "parameter-creation": @@ -329,10 +335,10 @@ def updateParameter(self, bp: ParameterBroadcastBluePrint): elif bp.action == "parameter-update" or bp.action == "parameter-call": item = self.findItems( - fullName, QtCore.Qt.MatchExactly | QtCore.Qt.MatchRecursive, 0 + fullName, QtCore.Qt.MatchFlag.MatchExactly | QtCore.Qt.MatchFlag.MatchRecursive, 0 # type: ignore[arg-type] ) if len(item) == 0: - if fullName not in self.itemsHide: + if fullName not in self.itemsHide: # type: ignore[operator] try: self.addItem( fullName, @@ -350,12 +356,14 @@ def updateParameter(self, bp: ParameterBroadcastBluePrint): # The model can't actually modify the widget since it knows nothing about the view itself. self.itemNewValue.emit(item[0].name, bp.value) - def insertItemTo(self, parent: QtGui.QStandardItem, item): + def insertItemTo( + self, parent: QtGui.QStandardItem, item: QtGui.QStandardItem + ) -> None: if item is not None: # A parameter might not have a unit unit = "" - if item.element is not None: - unit = item.element.unit + if item.element is not None: # type: ignore[attr-defined] + unit = item.element.unit # type: ignore[attr-defined] unitItem = QtGui.QStandardItem(unit) extraItem = QtGui.QStandardItem() @@ -371,7 +379,12 @@ def insertItemTo(self, parent: QtGui.QStandardItem, item): class ParametersTreeView(InstrumentTreeViewBase): - def __init__(self, model, *args, **kwargs): + def __init__( + self, + model: QtCore.QAbstractItemModel, + *args: Any, + **kwargs: Any, + ) -> None: super().__init__(model, [2], *args, **kwargs) self.delegate = ParameterDelegate(self) @@ -380,7 +393,7 @@ def __init__(self, model, *args, **kwargs): self.setAllDelegatesPersistent() @QtCore.Slot(object, object) - def onItemNewValue(self, itemName, value): + def onItemNewValue(self, itemName: str, value: Any) -> None: widget = self.delegate.parameters[itemName] try: # use the abstract set method defined in parameter widget so it works for different types of widgets @@ -394,12 +407,12 @@ def onItemNewValue(self, itemName, value): class InstrumentParameters(InstrumentDisplayBase): def __init__( self, - instrument, - parent=None, - viewType=ParametersTreeView, + instrument: Any, + parent: Optional[QtWidgets.QWidget] = None, + viewType: type = ParametersTreeView, callSignals: bool = True, - **kwargs, - ): + **kwargs: Any, + ) -> None: if "instrument" in kwargs: del kwargs["instrument"] modelKwargs = {} @@ -427,7 +440,7 @@ def __init__( **modelKwargs, ) - def connectSignals(self): + def connectSignals(self) -> None: super().connectSignals() self.model.itemNewValue.connect(self.view.onItemNewValue) @@ -442,22 +455,24 @@ class ParameterDeleteDelegate(ParameterDelegate): #: Emits the name of the parameter to be deleted when the user presses the delete button. removeParameter = QtCore.Signal(str) - def createEditor( + def createEditor( # type: ignore[override] self, widget: QtWidgets.QWidget, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex, ) -> QtWidgets.QWidget: item = self.getItem(index) - element = item.element - rw = self.makeRemoveWidget(item.name, widget) + element = item.element # type: ignore[attr-defined] + rw = self.makeRemoveWidget(item.name, widget) # type: ignore[attr-defined] ret = ParameterWidget(parameter=element, parent=widget, additionalWidgets=[rw]) - self.parameters[item.name] = ret + self.parameters[item.name] = ret # type: ignore[attr-defined] return ret - def makeRemoveWidget(self, fullName: str, widget: QtWidgets.QWidget): + def makeRemoveWidget( + self, fullName: str, widget: QtWidgets.QWidget + ) -> QtWidgets.QPushButton: w = QtWidgets.QPushButton(QtGui.QIcon(":/icons/delete.svg"), "", parent=widget) w.setStyleSheet(""" QPushButton { background-color: salmon } @@ -471,7 +486,12 @@ def makeRemoveWidget(self, fullName: str, widget: QtWidgets.QWidget): # TODO: Make sure that the refresh button refreshes the profiles as well as the model class ParameterManagerTreeView(InstrumentTreeViewBase): - def __init__(self, model, *args, **kwargs): + def __init__( + self, + model: QtCore.QAbstractItemModel, + *args: Any, + **kwargs: Any, + ) -> None: super().__init__(model, [2], *args, **kwargs) self.delegate = ParameterDeleteDelegate(self) @@ -480,7 +500,7 @@ def __init__(self, model, *args, **kwargs): self.setAllDelegatesPersistent() @QtCore.Slot(object, object) - def onItemNewValue(self, itemName, value): + def onItemNewValue(self, itemName: str, value: Any) -> None: widget = self.delegate.parameters[itemName] widget.paramWidget.setValue(value) @@ -490,11 +510,11 @@ class ProfilesManager(QtWidgets.QComboBox): #: Emitted when the selected index changed. indexChanged = QtCore.Signal() - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.setEditable(False) - self.params = self.parent().instrument + self.params = self.parent().instrument # type: ignore[union-attr] self.refreshing = False loadingProfile = None @@ -505,7 +525,7 @@ def __init__(self, *args, **kwargs): self.currentIndexChanged.connect(self.onCurrentIndexChanged) - def refresh(self): + def refresh(self) -> None: self.refreshing = True currentlySelected = self.currentText() self.clear() @@ -516,7 +536,7 @@ def refresh(self): self.refreshing = False @QtCore.Slot(int) - def onCurrentIndexChanged(self, index): + def onCurrentIndexChanged(self, index: int) -> None: if not self.refreshing: self.indexChanged.emit() @@ -533,9 +553,9 @@ class ParameterManagerGui(InstrumentParameters): def __init__( self, instrument: Union[ProxyInstrument, ParameterManager], - parent=None, - **kwargs, - ): + parent: Optional[QtWidgets.QWidget] = None, + **kwargs: Any, + ) -> None: super().__init__( instrument, parent=None, @@ -552,7 +572,7 @@ def __init__( self.connectSignals() self.loadProfile() - def connectSignals(self): + def connectSignals(self) -> None: super().connectSignals() self.view.delegate.removeParameter.connect(self.removeParameter) self.addParam.newParamRequested.connect(self.addParameter) @@ -560,7 +580,7 @@ def connectSignals(self): self.parameterCreated.connect(self.addParam.clear) self.profileManager.indexChanged.connect(self.loadProfile) - def makeToolbar(self): + def makeToolbar(self) -> QtWidgets.QToolBar: toolbar = super().makeToolbar() toolbar.addSeparator() @@ -569,26 +589,26 @@ def makeToolbar(self): QtGui.QIcon(":/icons/load.svg"), "Load parameters from file", ) - loadParamAction.triggered.connect(lambda x: self.loadFromFile()) + loadParamAction.triggered.connect(lambda x: self.loadFromFile()) # type: ignore[union-attr] saveParamAction = toolbar.addAction( QtGui.QIcon(":/icons/save.svg"), "Save parameters to file", ) - saveParamAction.triggered.connect(lambda x: self.saveToFile()) + saveParamAction.triggered.connect(lambda x: self.saveToFile()) # type: ignore[union-attr] return toolbar - def refreshAll(self): + def refreshAll(self) -> None: super().refreshAll() self.instrument.refresh_profiles() self.profileManager.refresh() - def removeParameter(self, fullName: str): + def removeParameter(self, fullName: str) -> None: if self.instrument.has_param(fullName): self.instrument.remove_parameter(fullName) - def addParameter(self, fullName, value, unit): + def addParameter(self, fullName: str, value: Any, unit: str) -> None: try: # Validators are commented out until they can be serialized. self.instrument.add_parameter( @@ -604,14 +624,14 @@ def addParameter(self, fullName, value, unit): return @QtCore.Slot() - def loadProfile(self): + def loadProfile(self) -> None: profileName = self.profileManager.currentText() self.instrument.switch_to_profile(profileName) super().refreshAll() self.instrument.refresh_profiles() @QtCore.Slot() - def loadFromFile(self, loadFile=None): + def loadFromFile(self, loadFile: Optional[str] = None) -> None: try: self.instrument.fromFile(filePath=loadFile, deleteMissing=False) self.refreshAll() @@ -620,7 +640,7 @@ def loadFromFile(self, loadFile=None): logger.info(f"Loading failed. {type(e)}: {e.args}") @QtCore.Slot() - def saveToFile(self): + def saveToFile(self) -> None: try: self.instrument.toFile() except Exception as e: @@ -633,12 +653,14 @@ def saveToFile(self): class MethodsModel(InstrumentModelBase): - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.setColumnCount(2) self.setHorizontalHeaderLabels([self.attr, "Arguments & Run"]) - def insertItemTo(self, parent, item): + def insertItemTo( + self, parent: QtGui.QStandardItem, item: QtGui.QStandardItem + ) -> None: if item is not None: extraItem = QtGui.QStandardItem() @@ -653,32 +675,37 @@ def insertItemTo(self, parent, item): class MethodsDelegate(DelegateBase): - def __init__(self, parent=None): + def __init__(self, parent: Optional[QtCore.QObject] = None) -> None: super().__init__(parent=parent) - self.methods = {} + self.methods: Dict[str, "MethodDisplay"] = {} - def createEditor( + def createEditor( # type: ignore[override] self, widget: QtWidgets.QWidget, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex, ) -> QtWidgets.QWidget: item = self.getItem(index) - element = item.element - ret = MethodDisplay(element, item.name, parent=widget) + element = item.element # type: ignore[attr-defined] + ret = MethodDisplay(element, item.name, parent=widget) # type: ignore[attr-defined] parent = self.parent() assert hasattr(parent, "clearAlertsAction") # connecting the widget with the clear alert signal - parent.clearAlertsAction.triggered.connect(ret.alertLabel.clearAlert) + parent.clearAlertsAction.triggered.connect(ret.alertLabel.clearAlert) # type: ignore[union-attr] - self.methods[item.name] = ret + self.methods[item.name] = ret # type: ignore[attr-defined] return ret class MethodsTreeView(InstrumentTreeViewBase): - def __init__(self, model, *args, **kwargs): + def __init__( + self, + model: QtCore.QAbstractItemModel, + *args: Any, + **kwargs: Any, + ) -> None: super().__init__(model, [1], *args, **kwargs) # Adding the clear alert to the context menu @@ -692,7 +719,7 @@ def __init__(self, model, *args, **kwargs): class InstrumentMethods(InstrumentDisplayBase): - def __init__(self, instrument, **kwargs): + def __init__(self, instrument: Any, **kwargs: Any) -> None: if "instrument" in kwargs: del kwargs["instrument"] @@ -722,8 +749,11 @@ class GenericInstrument(QtWidgets.QWidget): """ def __init__( - self, ins: Union[ProxyInstrument, Instrument], parent=None, **modelKwargs - ): + self, + ins: Union[ProxyInstrument, Instrument], + parent: Optional[QtWidgets.QWidget] = None, + **modelKwargs: Any, + ) -> None: super().__init__(parent=parent) self.ins = ins @@ -746,7 +776,7 @@ def __init__( self.setLayout(self._layout) self.splitter = QtWidgets.QSplitter(self) - self.splitter.setOrientation(QtCore.Qt.Vertical) + self.splitter.setOrientation(QtCore.Qt.Orientation.Vertical) self.parametersList = InstrumentParameters(instrument=ins, **modelKwargs) self.methodsList = InstrumentMethods(instrument=ins, **modelKwargs) @@ -762,7 +792,7 @@ def __init__( self.parametersList.view.resizeColumnToContents(1) self.methodsList.view.resizeColumnToContents(0) - def closeEvent(self, event: QtGui.QCloseEvent) -> None: + def closeEvent(self, event: QtGui.QCloseEvent) -> None: # type: ignore[override] """Stop the parameter subscriber thread before destruction.""" model = getattr(self.parametersList, "model", None) if model is not None and hasattr(model, "stopListener"): diff --git a/src/instrumentserver/gui/misc.py b/src/instrumentserver/gui/misc.py index 732e31d..65967ec 100644 --- a/src/instrumentserver/gui/misc.py +++ b/src/instrumentserver/gui/misc.py @@ -1,4 +1,4 @@ -from typing import Optional, Tuple +from typing import Any, Optional, Tuple from .. import QtCore, QtGui, QtWidgets @@ -11,20 +11,20 @@ def __init__( ): super().__init__(parent) - self.setAlignment(QtCore.Qt.AlignVCenter | QtCore.Qt.AlignHCenter) + self.setAlignment(QtCore.Qt.AlignmentFlag.AlignVCenter | QtCore.Qt.AlignmentFlag.AlignHCenter) # type: ignore[arg-type] self._pixmapSize = pixmapSize pix = QtGui.QIcon(":/icons/no-alert.svg").pixmap(*pixmapSize) self.setPixmap(pix) self.setToolTip("no alerts") @QtCore.Slot(str) - def setAlert(self, message: str): + def setAlert(self, message: str) -> None: pix = QtGui.QIcon(":/icons/red-alert.svg").pixmap(*self._pixmapSize) self.setPixmap(pix) self.setToolTip(message) @QtCore.Slot() - def clearAlert(self): + def clearAlert(self) -> None: pix = QtGui.QIcon(":/icons/no-alert.svg").pixmap(*self._pixmapSize) self.setPixmap(pix) self.setToolTip("no alerts") @@ -35,17 +35,17 @@ class AlertLabelGreen(AlertLabel): Expanding the functionality of the AlertLabel to add green alerts to indicate successful things """ - def mouseDoubleClickEvent(self, a0: QtGui.QMouseEvent) -> None: + def mouseDoubleClickEvent(self, a0: QtGui.QMouseEvent) -> None: # type: ignore[override] self.clearAlert() super().mouseDoubleClickEvent(a0) - def mousePressEvent(self, ev: QtGui.QMouseEvent) -> None: - if ev.buttons() == QtCore.Qt.MidButton: + def mousePressEvent(self, ev: QtGui.QMouseEvent) -> None: # type: ignore[override] + if ev.buttons() == QtCore.Qt.MouseButton.MidButton: self.clearAlert() super().mousePressEvent(ev) @QtCore.Slot(str) - def setSuccssefulAlert(self, message: str): + def setSuccssefulAlert(self, message: str) -> None: pix = QtGui.QIcon(":/icons/green-alert.svg").pixmap(*self._pixmapSize) self.setPixmap(pix) self.setToolTip(message) @@ -56,7 +56,13 @@ class DetachedTab(QtWidgets.QMainWindow): #: emitted when a tab for the instrument is closed onCloseSignal = QtCore.Signal(object, str) - def __init__(self, contentWidget: QtWidgets.QWidget, name: str, *args, **kwargs): + def __init__( + self, + contentWidget: QtWidgets.QWidget, + name: str, + *args: Any, + **kwargs: Any, + ) -> None: super().__init__(*args, **kwargs) self.name = name @@ -68,7 +74,7 @@ def __init__(self, contentWidget: QtWidgets.QWidget, name: str, *args, **kwargs) self.widget.show() - def closeEvent(self, a0: QtGui.QCloseEvent) -> None: + def closeEvent(self, a0: QtGui.QCloseEvent) -> None: # type: ignore[override] self.onCloseSignal.emit(self.widget, self.name) @@ -81,16 +87,16 @@ class SeparableTabBar(QtWidgets.QTabBar): #: Emitted when the user is moving the tabs. onMoveTab = QtCore.Signal(int, int) - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.selectedIndex = 0 self.dragStartPos = QtCore.QPoint() self.dragDroppedPos = QtCore.QPoint() - self.setElideMode(QtCore.Qt.ElideRight) + self.setElideMode(QtCore.Qt.TextElideMode.ElideRight) self.setAcceptDrops(True) - def mousePressEvent(self, a0: QtGui.QMouseEvent) -> None: - if a0.button() == QtCore.Qt.LeftButton: + def mousePressEvent(self, a0: QtGui.QMouseEvent) -> None: # type: ignore[override] + if a0.button() == QtCore.Qt.MouseButton.LeftButton: self.dragStartPos = a0.pos() self.dragDroppedPos.setX(0) @@ -99,11 +105,11 @@ def mousePressEvent(self, a0: QtGui.QMouseEvent) -> None: self.selectedIndex = self.tabAt(self.dragStartPos) super().mousePressEvent(a0) - def mouseDoubleClickEvent(self, a0: QtGui.QMouseEvent) -> None: + def mouseDoubleClickEvent(self, a0: QtGui.QMouseEvent) -> None: # type: ignore[override] self.onDetachTab.emit(self.tabAt(a0.pos()), a0.globalPos()) a0.accept() - def mouseMoveEvent(self, a0: QtGui.QMouseEvent) -> None: + def mouseMoveEvent(self, a0: QtGui.QMouseEvent) -> None: # type: ignore[override] """ Detects if the user is dragging a tab and starts the drag object. """ @@ -118,49 +124,49 @@ def mouseMoveEvent(self, a0: QtGui.QMouseEvent) -> None: mimeData.setData("action", b"application/tab-detach") drag.setMimeData(mimeData) - pixmap = self.parentWidget().currentWidget().grab() # type: ignore[attr-defined] # I am pretty sure the stubs are wrong for this one, running through the debugger all the methods exists. + pixmap = self.parentWidget().currentWidget().grab() # type: ignore[union-attr] targetPixmap = QtGui.QPixmap(pixmap.size()) - targetPixmap.fill(QtCore.Qt.transparent) + targetPixmap.fill(QtCore.Qt.GlobalColor.transparent) painter = QtGui.QPainter(targetPixmap) painter.drawPixmap(0, 0, pixmap) painter.end() drag.setPixmap(targetPixmap) - dropAction = drag.exec_(QtCore.Qt.MoveAction | QtCore.Qt.CopyAction) + dropAction = drag.exec_(QtCore.Qt.DropAction.MoveAction | QtCore.Qt.DropAction.CopyAction) # type: ignore[call-overload] # In linux the drag.exec_ does not return MoveAction, so it must be set manually. if self.dragDroppedPos.x() != 0 and self.dragDroppedPos.y() != 0: - dropAction = QtCore.Qt.MoveAction + dropAction = QtCore.Qt.DropAction.MoveAction # A move action indicates that the user is trying to move the tabs around - if dropAction == QtCore.Qt.MoveAction: + if dropAction == QtCore.Qt.DropAction.MoveAction: a0.accept() self.onMoveTab.emit( self.tabAt(self.dragStartPos), self.tabAt(self.dragDroppedPos) ) # An ignore action means that the user dropped the tab outside of the window and should be detached. - elif dropAction == QtCore.Qt.IgnoreAction: + elif dropAction == QtCore.Qt.DropAction.IgnoreAction: a0.accept() self.onDetachTab.emit(self.selectedIndex, self.cursor().pos()) else: super().mouseMoveEvent(a0) - def dragEnterEvent(self, a0: QtGui.QDragEnterEvent) -> None: + def dragEnterEvent(self, a0: QtGui.QDragEnterEvent) -> None: # type: ignore[override] mimeData = a0.mimeData() - formats = mimeData.formats() + formats = mimeData.formats() # type: ignore[union-attr] - if "action" in formats and mimeData.data("action") == "application/tab-detach": + if "action" in formats and mimeData.data("action") == "application/tab-detach": # type: ignore[union-attr] a0.acceptProposedAction() super().dragMoveEvent(a0) - def dropEvent(self, a0: QtGui.QDropEvent) -> None: + def dropEvent(self, a0: QtGui.QDropEvent) -> None: # type: ignore[override] self.dragDroppedPos = a0.pos() super().dropEvent(a0) - def mouseReleaseEvent(self, a0: QtGui.QMouseEvent) -> None: + def mouseReleaseEvent(self, a0: QtGui.QMouseEvent) -> None: # type: ignore[override] a0.accept() super().mouseReleaseEvent(a0) @@ -176,7 +182,7 @@ class DetachableTabWidget(QtWidgets.QTabWidget): #: Emitted when a tab got closed. onTabClosed = QtCore.Signal(str) - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self._tabBar = SeparableTabBar(self) self._tabBar.setTabsClosable(True) @@ -185,9 +191,9 @@ def __init__(self, *args, **kwargs): self._tabBar.onDetachTab.connect(self.onDetachTab) self._tabBar.onMoveTab.connect(self.onMoveTab) - self.unclosableTabs = {} + self.unclosableTabs: dict[str, QtWidgets.QWidget] = {} - def addUnclosableTab(self, widget, name): + def addUnclosableTab(self, widget: QtWidgets.QWidget, name: str) -> None: index = self.addTab(widget, name) closeButton = self._tabBar.tabButton( index, QtWidgets.QTabBar.ButtonPosition.RightSide @@ -197,25 +203,25 @@ def addUnclosableTab(self, widget, name): closeButton = self._tabBar.tabButton( index, QtWidgets.QTabBar.ButtonPosition.LeftSide ) - closeButton.resize(0, 0) + closeButton.resize(0, 0) # type: ignore[union-attr] self.unclosableTabs[name] = widget @QtCore.Slot(object, object) - def onDetachTab(self, tab, point: QtCore.QPoint): + def onDetachTab(self, tab: int, point: QtCore.QPoint) -> None: """ Gets triggered when the user drags out a tab. Opens a QMainWindow with the widget in the dragged tab. """ widget = self.widget(tab) name = self.tabText(tab) self.removeTab(self.indexOf(widget)) - detachedTab = DetachedTab(widget, name, parent=self) + detachedTab = DetachedTab(widget, name, parent=self) # type: ignore[arg-type] movedPoint = QtCore.QPoint(point.x(), point.y()) detachedTab.move(movedPoint) detachedTab.onCloseSignal.connect(self.onAttatchTab) detachedTab.show() @QtCore.Slot(object, str) - def onAttatchTab(self, widget, name): + def onAttatchTab(self, widget: QtWidgets.QWidget, name: str) -> None: """ Gets called when the user closes one of the detachable windows and properly attaches the tab back. """ @@ -225,7 +231,7 @@ def onAttatchTab(self, widget, name): self.addTab(widget, name) @QtCore.Slot(int, int) - def onMoveTab(self, fromIndex, toIndex): + def onMoveTab(self, fromIndex: int, toIndex: int) -> None: widget = self.widget(fromIndex) icon = self.tabIcon(fromIndex) text = self.tabText(fromIndex) @@ -235,11 +241,11 @@ def onMoveTab(self, fromIndex, toIndex): if text in self.unclosableTabs: self._tabBar.tabButton( toIndex, QtWidgets.QTabBar.ButtonPosition.RightSide - ).resize(0, 0) + ).resize(0, 0) # type: ignore[union-attr] self.setCurrentWidget(widget) @QtCore.Slot(int) - def onCloseTab(self, index, moving=False): + def onCloseTab(self, index: int, moving: bool = False) -> None: """ Closes the tab at index. @@ -248,7 +254,7 @@ def onCloseTab(self, index, moving=False): """ name = self.tabText(index) widget = self.widget(index) - widget.close() + widget.close() # type: ignore[union-attr] widget = None self.removeTab(index) @@ -268,14 +274,14 @@ class BaseDialog(QtWidgets.QDialog): def __init__( self, - parent=None, - flags=(QtCore.Qt.CustomizeWindowHint | QtCore.Qt.WindowCloseButtonHint), - tittleBarButtonsWidth=108, - ): + parent: Optional[QtWidgets.QWidget] = None, + flags: Any = (QtCore.Qt.WindowType.CustomizeWindowHint | QtCore.Qt.WindowType.WindowCloseButtonHint), + tittleBarButtonsWidth: int = 108, + ) -> None: super().__init__(parent, flags=flags) self.tittleBarButtonsWidth = tittleBarButtonsWidth - def setWindowTitle(self, p_str): + def setWindowTitle(self, p_str: Optional[str]) -> None: super().setWindowTitle(p_str) tittleWidth = self.fontMetrics().boundingRect(p_str).size().width() minWidth = self.tittleBarButtonsWidth + tittleWidth + 15 diff --git a/src/instrumentserver/gui/parameters.py b/src/instrumentserver/gui/parameters.py index f7fa9ea..c1d7f15 100644 --- a/src/instrumentserver/gui/parameters.py +++ b/src/instrumentserver/gui/parameters.py @@ -1,7 +1,7 @@ import logging import numbers import re -from typing import Any, Callable, List, Optional +from typing import Any, Callable, List, Optional, Tuple from qcodes import Parameter @@ -18,7 +18,7 @@ FLOAT_PRECISION = 10 # The maximum number of significant digits for float numbers -def float_formater(val): +def float_formater(val: Any) -> str: """ For displaying float numbers with scientific notation. """ @@ -52,9 +52,9 @@ class ParameterWidget(QtWidgets.QWidget): def __init__( self, parameter: Parameter, - parent=None, + parent: Optional[QtWidgets.QWidget] = None, additionalWidgets: Optional[List[QtWidgets.QWidget]] = None, - ): + ) -> None: super().__init__(parent) @@ -177,13 +177,13 @@ def __init__( self.setLayout(layout) @QtCore.Slot() - def onReturnPressed(self): + def onReturnPressed(self) -> None: """Activates the setButton when the input is selected and enter is pressed.""" self.setButton.click() self.paramWidget.input.deselect() self.setButton.setFocus() - def setParameter(self, value: Any): + def setParameter(self, value: Any) -> None: try: self._parameter.set(value) except Exception as e: @@ -194,15 +194,15 @@ def setParameter(self, value: Any): self.parameterSet.emit(value) - def setPending(self, value: Any): + def setPending(self, value: Any) -> None: self.parameterPending.emit(value) @QtCore.Slot() - def getAndEmitValueFromWidget(self): + def getAndEmitValueFromWidget(self) -> None: self._valueFromWidget.emit(self._getMethod()) @QtCore.Slot() - def setWidgetFromParameter(self): + def setWidgetFromParameter(self) -> None: val = self._parameter.get() self._setMethod(val) self.parameterSet.emit(val) @@ -213,7 +213,7 @@ class AnyInput(QtWidgets.QWidget): #: emitted when the input field is changed, argument is the new value. inputChanged = QtCore.Signal(str) - def __init__(self, parent=None): + def __init__(self, parent: Optional[QtWidgets.QWidget] = None) -> None: super().__init__(parent) self.input = QtWidgets.QLineEdit() @@ -242,7 +242,7 @@ def __init__(self, parent=None): QPushButton:checked { background-color: palegreen } """) - def value(self): + def value(self) -> Any: if self.doEval.isChecked(): try: ret = eval(self.input.text()) @@ -252,7 +252,7 @@ def value(self): else: return self.input.text() - def setValue(self, val: Any): + def setValue(self, val: Any) -> None: try: self.input.setText(float_formater(val)) except RuntimeError as e: @@ -261,18 +261,18 @@ def setValue(self, val: Any): ) @QtCore.Slot(str) - def _processTextEdited(self, val: str): + def _processTextEdited(self, val: str) -> None: self.inputChanged.emit(val) class NumberInput(QtWidgets.QLineEdit): """A text edit widget that checks whether its input can be read as a number.""" - def __init__(self, parent=None): + def __init__(self, parent: Optional[QtWidgets.QWidget] = None) -> None: super().__init__(parent) self.textChanged.connect(self.checkIfNumber) - def checkIfNumber(self, value: str): + def checkIfNumber(self, value: str) -> None: try: val = eval(value) except Exception: @@ -287,7 +287,7 @@ def checkIfNumber(self, value: str): NumberInput { } """) - def value(self): + def value(self) -> Optional[numbers.Number]: try: value = eval(self.text()) except Exception: @@ -297,7 +297,7 @@ def value(self): else: return None - def setValue(self, value: numbers.Number): + def setValue(self, value: numbers.Number) -> None: try: self.setText(float_formater(value)) except RuntimeError as e: @@ -316,7 +316,7 @@ class AnyInputForMethod(AnyInput): a long string. """ - def value(self): + def value(self) -> Tuple[Any, Any]: if self.doEval.isChecked(): # If '=' is present we need to separate the keyword from the value # If ',' is present we have more than one argument. @@ -340,7 +340,7 @@ def value(self): class SetButton(QtWidgets.QPushButton): @QtCore.Slot(bool) - def setPending(self, isPending: bool): + def setPending(self, isPending: bool) -> None: if isPending: self.setStyleSheet("SetButton { background-color: orange }") else: diff --git a/src/instrumentserver/helpers.py b/src/instrumentserver/helpers.py index 09bb205..c4e2e89 100644 --- a/src/instrumentserver/helpers.py +++ b/src/instrumentserver/helpers.py @@ -46,11 +46,11 @@ def stringToArgsAndKwargs(value: str) -> Tuple[List[Any], Dict[str, Any]]: return args, kwargs -def typeClassPath(t) -> str: +def typeClassPath(t: type) -> str: return f"{t.__module__}.{t.__qualname__}" -def objectClassPath(o) -> str: +def objectClassPath(o: Any) -> str: return f"{o.__class__.__module__}.{o.__class__.__qualname__}" @@ -134,7 +134,7 @@ def flat_to_nested_dict(flat_dict: Dict) -> Dict: # result: {"a": {"b": {"c": 1,"d": 2}},"x": 3} """ - nested = {} + nested: Dict[str, Any] = {} for key, value in flat_dict.items(): parts = key.split(".") d = nested @@ -151,7 +151,7 @@ def is_flat_dict(d: dict) -> bool: return all(not isinstance(v, dict) for v in d.values()) -def flatten_dict(d, sep="."): +def flatten_dict(d: Dict[str, Any], sep: str = ".") -> Dict[str, Any]: """ Detects if a dictionary is flat (i.e. all values are non-dicts). If it is not flat, recursively flattens it using dot-separated keys. @@ -174,8 +174,8 @@ def flatten_dict(d, sep="."): if is_flat_dict(d): return d - def flatten(nested, parent_key=""): - items = {} + def flatten(nested: Dict[str, Any], parent_key: str = "") -> Dict[str, Any]: + items: Dict[str, Any] = {} for k, v in nested.items(): new_key = f"{parent_key}{sep}{k}" if parent_key else k if isinstance(v, dict): diff --git a/src/instrumentserver/log.py b/src/instrumentserver/log.py index 4d2ea43..05cdefc 100644 --- a/src/instrumentserver/log.py +++ b/src/instrumentserver/log.py @@ -7,6 +7,7 @@ import sys from enum import Enum, auto, unique from html import escape +from typing import Callable, Optional from . import QtCore, QtGui, QtWidgets @@ -31,33 +32,35 @@ class QLogHandler(QtCore.QObject, logging.Handler): new_html = QtCore.Signal(str) - def __init__(self, parent): + def __init__(self, parent: Optional[QtWidgets.QWidget]) -> None: QtCore.QObject.__init__(self, parent) logging.Handler.__init__(self) self.widget = QtWidgets.QTextEdit(parent) self.widget.setReadOnly(True) - self._transform = None + self._transform: Optional[Callable[[logging.LogRecord, str], Optional[str]]] = None # connect signal to slot that actually touches the widget (GUI thread) self.new_html.connect(self._append_html) @QtCore.Slot(str) - def _append_html(self, html: str): + def _append_html(self, html: str) -> None: """Append HTML to the text widget in the GUI thread.""" self.widget.append(html) # reset char format so bold/italics don’t bleed into the next line self.widget.setCurrentCharFormat(QtGui.QTextCharFormat()) # keep view scrolled to bottom - self.widget.verticalScrollBar().setValue( - self.widget.verticalScrollBar().maximum() + self.widget.verticalScrollBar().setValue( # type: ignore[union-attr] + self.widget.verticalScrollBar().maximum() # type: ignore[union-attr] ) - def set_transform(self, fn): + def set_transform( + self, fn: Callable[[logging.LogRecord, str], Optional[str]] + ) -> None: """fn(record, msg) -> str | {'html': str} | None""" self._transform = fn - def emit(self, record): + def emit(self, record: logging.LogRecord) -> None: try: formatted = self.format(record) # prefix + message raw_msg = record.getMessage() # message only @@ -108,7 +111,9 @@ class LogWidget(QtWidgets.QWidget): The handler has the actual widget that is used to display the logs. """ - def __init__(self, parent=None, level=logging.INFO): + def __init__( + self, parent: Optional[QtWidgets.QWidget] = None, level: int = logging.INFO + ) -> None: super().__init__(parent) # set up the graphical handler @@ -142,7 +147,9 @@ def __init__(self, parent=None, level=logging.INFO): self.handler.set_transform(_param_update_formatter) -def _param_update_formatter(record, raw_msg): +def _param_update_formatter( + record: logging.LogRecord, raw_msg: str +) -> Optional[str]: """ A formater that makes parameter updates more prominent in the gui log window. """ @@ -168,11 +175,11 @@ def _param_update_formatter(record, raw_msg): def setupLogging( - addStreamHandler=True, - logFile=None, - name="instrumentserver", - streamHandlerLevel=logging.INFO, -): + addStreamHandler: bool = True, + logFile: Optional[str] = None, + name: str = "instrumentserver", + streamHandlerLevel: int = logging.INFO, +) -> None: """Setting up logging, including adding a custom handler.""" logger = logging.getLogger(name) @@ -204,12 +211,12 @@ def setupLogging( logger.info(f"Logging set up for {name}.") -def logger(name="instrumentserver"): +def logger(name: str = "instrumentserver") -> logging.Logger: """Get the (root) logger for the package.""" return logging.getLogger(name) -def log(logger, message, level): +def log(logger: logging.Logger, message: str, level: LogLevels) -> None: """Simple wrapper to log messages. Useful when the log level is a variable. diff --git a/src/instrumentserver/monitoring/listener.py b/src/instrumentserver/monitoring/listener.py index 1bd55b8..6e7a26c 100644 --- a/src/instrumentserver/monitoring/listener.py +++ b/src/instrumentserver/monitoring/listener.py @@ -1,19 +1,23 @@ import argparse import logging import os.path -from abc import ABC +from abc import ABC, abstractmethod from dataclasses import dataclass from datetime import datetime from pathlib import Path -from typing import Any, Dict +from typing import Any, Dict, Optional from zoneinfo import ZoneInfo, ZoneInfoNotFoundError import pandas as pd -import ruamel.yaml # type: ignore[import-untyped] # Known bugfix under no-fix status: https://sourceforge.net/p/ruamel-yaml/tickets/328/ +import ruamel.yaml import zmq try: - from influxdb_client import InfluxDBClient, Point, WriteOptions + from influxdb_client import ( # type: ignore[import-not-found] + InfluxDBClient, + Point, + WriteOptions, + ) except ImportError: pass @@ -25,10 +29,13 @@ class Listener(ABC): - def __init__(self, addresses: list): + def __init__(self, addresses: list) -> None: self.addresses = addresses - def run(self): + @abstractmethod + def listenerEvent(self, *args: Any, **kwargs: Any) -> None: ... + + def run(self) -> None: # creates zmq subscriber at specified address logger.info(f"Connecting to {self.addresses}") @@ -67,7 +74,7 @@ class CSVConfig: csv_path: str @classmethod - def from_dict(cls, config_dict): + def from_dict(cls, config_dict: Dict[str, Any]) -> "CSVConfig": return cls( addresses=config_dict["addresses"], params=config_dict["params"], @@ -87,7 +94,7 @@ class InfluxConfig: timezone_name: str = "CDT" @classmethod - def from_dict(cls, config_dict): + def from_dict(cls, config_dict: Dict[str, Any]) -> "InfluxConfig": return cls( addresses=config_dict["addresses"], params=config_dict["params"], @@ -100,7 +107,7 @@ def from_dict(cls, config_dict): class DFListener(Listener): - def __init__(self, csvConfig: CSVConfig): + def __init__(self, csvConfig: CSVConfig) -> None: super().__init__(csvConfig.addresses) self.addresses = csvConfig.addresses self.path = csvConfig.csv_path @@ -115,10 +122,10 @@ def __init__(self, csvConfig: CSVConfig): self.paramList = list(csvConfig.params) - def run(self): + def run(self) -> None: super().run() - def listenerEvent(self, message: ParameterBroadcastBluePrint): + def listenerEvent(self, message: ParameterBroadcastBluePrint) -> None: # listens only for parameters in the list, if it is empty, it listens to everything if not self.paramList: @@ -142,7 +149,7 @@ def listenerEvent(self, message: ParameterBroadcastBluePrint): class InfluxListener(Listener): - def __init__(self, influxConfig: InfluxConfig): + def __init__(self, influxConfig: InfluxConfig) -> None: super().__init__(influxConfig.addresses) self.addresses = influxConfig.addresses @@ -158,10 +165,12 @@ def __init__(self, influxConfig: InfluxConfig): self.timezone_info = get_timezone_info(influxConfig.timezone_name) - def run(self): + def run(self) -> None: super().run() - def listenerEvent(self, instrument, message: ParameterBroadcastBluePrint): + def listenerEvent( + self, instrument: str, message: ParameterBroadcastBluePrint + ) -> None: bucket = self.bucketDict[instrument] measurementName = self.measurementNameDict[instrument] # listens only for parameters in the list, if it is empty, it listens to everything @@ -176,7 +185,7 @@ def listenerEvent(self, instrument, message: ParameterBroadcastBluePrint): self.write_api.write(bucket=bucket, org=self.org, record=point) -def checkInfluxConfig(configInput: Dict[str, Any]): +def checkInfluxConfig(configInput: Dict[str, Any]) -> bool: # check if all fields are present in the config file influxFields = [ @@ -197,7 +206,7 @@ def checkInfluxConfig(configInput: Dict[str, Any]): return True -def checkCSVConfig(configInput: Dict[str, Any]): +def checkCSVConfig(configInput: Dict[str, Any]) -> bool: # check if all fields are present in the config file csvField = ["addresses", "params", "csv_path"] @@ -208,7 +217,7 @@ def checkCSVConfig(configInput: Dict[str, Any]): return True -def get_timezone_info(timezone_name): +def get_timezone_info(timezone_name: str) -> Optional[ZoneInfo]: try: return ZoneInfo(timezone_name) except ZoneInfoNotFoundError: @@ -216,7 +225,7 @@ def get_timezone_info(timezone_name): return None -def startListener(): +def startListener() -> None: parser = argparse.ArgumentParser(description="Starting the listener") parser.add_argument("-c", "--config") @@ -230,7 +239,7 @@ def startListener(): configInput = yaml.load(configPath) else: logger.warning("Please enter a valid path for the config file") - return 0 + return # start listener that writes to CSV or Influx Database if "type" in configInput: diff --git a/src/instrumentserver/params.py b/src/instrumentserver/params.py index ccb1f15..41d204c 100644 --- a/src/instrumentserver/params.py +++ b/src/instrumentserver/params.py @@ -72,28 +72,28 @@ class ParameterManager(InstrumentBase): # TODO: method to instantiate entirely from paramDict - def __init__(self, name): + def __init__(self, name: str) -> None: super().__init__(name) self._workingDirectory = Path(os.getcwd()) #: default location and name of the parameters save file. self.selectedProfile = self.fullProfileName(self.name) - self.profiles = [] + self.profiles: List[str] = [] self.refresh_profiles() self.fromFile() @property - def workingDirectory(self): + def workingDirectory(self) -> Path: return self._workingDirectory @workingDirectory.setter - def workingDirectory(self, path: Union[str, Path]): + def workingDirectory(self, path: Union[str, Path]) -> None: self._workingDirectory = Path(path) self.refresh_profiles() - def getWorkingDirectory(self): + def getWorkingDirectory(self): # type: ignore[no-untyped-def] return self.workingDirectory @staticmethod @@ -138,7 +138,7 @@ def _to_tree(cls, pm: "ParameterManager") -> Dict: return ret @classmethod - def does_profile_exist(cls, profiles, target): + def does_profile_exist(cls, profiles: List[str], target: str) -> bool: found = False for profile in profiles: if target in profile: @@ -160,7 +160,7 @@ def refresh_profiles(self) -> List[str]: self.profiles = profiles return profiles - def to_tree(self): + def to_tree(self) -> Dict: return ParameterManager._to_tree(self) def _get_param(self, param_name: str) -> ParameterBase: @@ -187,20 +187,20 @@ def _get_parent( ) if n not in parent.submodules: if create_parent: - parent.add_submodule(n, ParameterManager(n)) # type: ignore # This one is technically breaking the type hints from qcodes itself, but it seems to work fine. + parent.add_submodule(n, ParameterManager(n)) # type: ignore[type-var] else: raise ValueError(f"{n} does not exist.") - parent = parent.submodules[n] # type: ignore # This one is technically breaking the type hints from qcodes itself, but it seems to work fine. + parent = parent.submodules[n] # type: ignore[assignment] return parent - def has_param(self, param_name: str): + def has_param(self, param_name: str) -> bool: try: self._get_param(param_name) return True except ValueError: return False - def add_parameter(self, name: str, **kw: Any) -> None: # type: ignore # Breaks LSP principle, code works, don't want to change it. + def add_parameter(self, name: str, **kw: Any) -> None: # type: ignore[override] """Add a parameter. :param name: Name of the parameter. @@ -227,7 +227,7 @@ def add_parameter(self, name: str, **kw: Any) -> None: # type: ignore # Breaks else: parent.add_parameter(name.split(".")[-1], **kw) - def remove_parameter(self, param_name: str, cleanup: bool = True): + def remove_parameter(self, param_name: str, cleanup: bool = True) -> None: parent = self._get_parent(param_name) pname = param_name.split(".")[-1] del parent.parameters[pname] @@ -242,27 +242,27 @@ def set(self, param_name: str, value: Any) -> Any: param = self._get_param(param_name) param.set(value) - def remove_empty_submodules(self): + def remove_empty_submodules(self) -> None: """Delete all empty submodules in the instrument.""" - def is_empty(parent): + def is_empty(parent: InstrumentBase) -> bool: if len(parent.submodules) == 0 and len(parent.parameters) == 0: return True else: return False - def purge(parent): + def purge(parent: InstrumentBase) -> None: mark_for_deletion = [] for n, s in parent.submodules.items(): - purge(s) - if is_empty(s): + purge(s) # type: ignore[arg-type] + if is_empty(s): # type: ignore[arg-type] mark_for_deletion.append(n) for n in mark_for_deletion: del parent.submodules[n] purge(self) - def remove_all_parameters(self): + def remove_all_parameters(self) -> None: """Remove all parameters from the instrument.""" for param in self.list(): self.remove_parameter(param, cleanup=False) @@ -280,7 +280,7 @@ def list(self) -> List[str]: """Return a list of all parameters.""" tree = self.to_tree() - def tolist(x): + def tolist(x: Dict[str, Any]) -> List[str]: ret_ = [] for k, v in x.items(): if isinstance(v, Parameter): @@ -291,7 +291,11 @@ def tolist(x): return tolist(tree) - def fromFile(self, filePath: str | None = None, deleteMissing: bool = True): + def fromFile( + self, + filePath: str | None = None, + deleteMissing: bool = True, + ) -> None: """Load parameters from a parameter json file (see :mod:`.serialize`). @@ -304,8 +308,10 @@ def fromFile(self, filePath: str | None = None, deleteMissing: bool = True): ParameterManager that are not listed in the file. """ if filePath is None: - filePath = self.workingDirectory.joinpath( - self.fullProfileName(self.selectedProfile) + filePath = str( + self.workingDirectory.joinpath( + self.fullProfileName(self.selectedProfile) + ) ) if os.path.exists(filePath): @@ -326,7 +332,9 @@ def fromFile(self, filePath: str | None = None, deleteMissing: bool = True): else: logger.warning("parameter file not found, cannot load.") - def fromParamDict(self, paramDict: Dict[str, Any], deleteMissing: bool = True): + def fromParamDict( + self, paramDict: Dict[str, Any], deleteMissing: bool = True + ) -> None: """Load parameters from a parameter dictionary (see :mod:`.serialize`). :param paramDict: Parameter dictionary. @@ -370,13 +378,17 @@ def fromParamDict(self, paramDict: Dict[str, Any], deleteMissing: bool = True): def toParamDict( self, simpleFormat: bool = False, includeMeta: List[str] = ["unit"] - ): + ) -> Dict[str, Any]: params = serialize.toParamDict( [self], simpleFormat=simpleFormat, includeMeta=includeMeta ) return params - def toFile(self, filePath: str | None = None, name: str | None = None): + def toFile( + self, + filePath: str | None = None, + name: str | None = None, + ) -> None: """Save parameters from the instrument into a json file. If the file being saved is a profile file (starts with 'parameter_manager-' and ends with '.json'), the selectedProfile is changed to the filename. @@ -391,7 +403,7 @@ def toFile(self, filePath: str | None = None, name: str | None = None): """ if filePath is None: - filePath = self.workingDirectory + filePath = str(self.workingDirectory) if os.path.isdir(filePath): if name is None: @@ -415,7 +427,7 @@ def list_profiles(self) -> List[str]: """ return self.profiles - def switch_to_profile(self, profile: str): + def switch_to_profile(self, profile: str) -> None: """ Switches the server to the passed profile. """ diff --git a/src/instrumentserver/serialize.py b/src/instrumentserver/serialize.py index 4a04055..59c4554 100644 --- a/src/instrumentserver/serialize.py +++ b/src/instrumentserver/serialize.py @@ -191,7 +191,7 @@ def fromParamDict(paramDict: Dict[str, Any], target: SerializableType) -> None: # Tools -def isSimpleFormat(paramDict: Dict[str, Any]): +def isSimpleFormat(paramDict: Dict[str, Any]) -> bool: """Checks if the supplied paramDict is in the simplified format. We identify the simple format by the fact that otherwise **all** item values @@ -204,7 +204,7 @@ def isSimpleFormat(paramDict: Dict[str, Any]): return False -def validateParamDict(params: Dict[str, Any]): +def validateParamDict(params: Dict[str, Any]) -> None: if isSimpleFormat(params): return @@ -216,7 +216,7 @@ def validateParamDict(params: Dict[str, Any]): raise -def toDataFrame(input: SerializableType): +def toDataFrame(input: SerializableType) -> "pd.DataFrame": """Make a pandas data frame from the parameters. Mainly useful for printing overviews in notebooks.""" params = toParamDict(input, includeMeta=["unit", "vals"]) @@ -282,10 +282,9 @@ def _singleInstrumentParametersToJson( for name, submod in instrument.submodules.items(): ret.update( _singleInstrumentParametersToJson( - # FIXME: Fix this mypy ignore - submod, + submod, # type: ignore[arg-type] get=get, - addPrefix=f"{addPrefix + name}.", # type: ignore[arg-type] + addPrefix=f"{addPrefix + name}.", simpleFormat=simpleFormat, includeMeta=includeMeta, ) @@ -311,7 +310,7 @@ def _getParamFromList(parent: Any, childrenList: List[str]) -> Parameter: return _getParamFromList(nextObj, childrenList[1:]) -def _getObjectByName(name: str, src: SerializableType): +def _getObjectByName(name: str, src: SerializableType) -> Any: """Get an object from a container by specifying its name.""" if isinstance(src, Station): diff --git a/src/instrumentserver/server/application.py b/src/instrumentserver/server/application.py index a38f6da..2928048 100644 --- a/src/instrumentserver/server/application.py +++ b/src/instrumentserver/server/application.py @@ -4,7 +4,7 @@ import os import sys import time -from typing import Any, Dict, Optional, Union +from typing import Any, Dict, List, Optional, Tuple, Union from instrumentserver.client import QtClient from instrumentserver.log import LogLevels, LogWidget, log @@ -42,7 +42,7 @@ class StationList(QtWidgets.QTreeWidget): #: Argument is the name of the instrument that should be closed closeRequested = QtCore.Signal(str) - def __init__(self, parent=None): + def __init__(self, parent: Optional[QtWidgets.QWidget] = None) -> None: super().__init__(parent) self.setColumnCount(len(self.cols)) @@ -57,22 +57,22 @@ def __init__(self, parent=None): self.contextMenu = QtWidgets.QMenu(self) self.contextMenu.addAction(self.deleteAction) - self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu) self.customContextMenuRequested.connect( - lambda x: self.contextMenu.exec_(self.mapToGlobal(x)) + lambda x: self.contextMenu.exec_(self.mapToGlobal(x)) # type: ignore[arg-type] ) self.deleteAction.triggered.connect(self.onDeleteAction) self.itemSelectionChanged.connect(self._processSelection) - def addInstrument(self, bp: InstrumentModuleBluePrint): + def addInstrument(self, bp: InstrumentModuleBluePrint) -> None: lst = [bp.name, f"{bp.instrument_module_class.split('.')[-1]}"] self.addTopLevelItem(QtWidgets.QTreeWidgetItem(lst)) self.resizeColumnToContents(0) - def removeObject(self, name: str): + def removeObject(self, name: str) -> None: items = self.findItems( - name, QtCore.Qt.MatchExactly | QtCore.Qt.MatchRecursive, 0 + name, QtCore.Qt.MatchFlag.MatchExactly | QtCore.Qt.MatchFlag.MatchRecursive, 0 # type: ignore[arg-type] ) if len(items) > 0: item = items[0] @@ -80,7 +80,7 @@ def removeObject(self, name: str): self.takeTopLevelItem(idx) del item - def _processSelection(self): + def _processSelection(self) -> None: items = self.selectedItems() if len(items) == 0: return @@ -88,7 +88,7 @@ def _processSelection(self): self.componentSelected.emit(item.text(0)) @QtCore.Slot() - def onDeleteAction(self): + def onDeleteAction(self) -> None: # need to check if widget has focus because of the keyboard shortcuts if self.hasFocus(): items = self.selectedItems() @@ -108,23 +108,23 @@ def onDeleteAction(self): class StationObjectInfo(QtWidgets.QTextEdit): - def __init__(self, parent=None): + def __init__(self, parent: Optional[QtWidgets.QWidget] = None) -> None: super().__init__(parent) self.setReadOnly(True) @QtCore.Slot(object) - def setObject(self, bp: InstrumentModuleBluePrint): + def setObject(self, bp: InstrumentModuleBluePrint) -> None: self.setHtml(bluePrintToHtml(bp)) class ServerStatus(QtWidgets.QWidget): """A widget that shows the status of the instrument server.""" - def __init__(self, parent=None): + def __init__(self, parent: Optional[QtWidgets.QWidget] = None) -> None: super().__init__(parent) - self.layout = QtWidgets.QVBoxLayout(self) + self.layout = QtWidgets.QVBoxLayout(self) # type: ignore[assignment,method-assign] # At the top: a status label, and a button for emitting a test message self.addressLabel = QtWidgets.QLabel() @@ -138,20 +138,20 @@ def __init__(self, parent=None): ) ) - self.layout.addLayout(self.statusLayout) + self.layout.addLayout(self.statusLayout) # type: ignore[attr-defined] # next row: a window for displaying the incoming messages. - self.layout.addWidget(QtWidgets.QLabel("Messages:")) + self.layout.addWidget(QtWidgets.QLabel("Messages:")) # type: ignore[attr-defined] self.messages = QtWidgets.QTextEdit() self.messages.setReadOnly(True) - self.layout.addWidget(self.messages) + self.layout.addWidget(self.messages) # type: ignore[attr-defined] @QtCore.Slot(str) - def setListeningAddress(self, addr: str): + def setListeningAddress(self, addr: str) -> None: self.addressLabel.setText(f"Listening on: {addr}") @QtCore.Slot(str, str) - def addMessageAndReply(self, message: str, reply: str): + def addMessageAndReply(self, message: str, reply: str) -> None: tstr = time.strftime("%Y-%m-%d %H:%M:%S") self.messages.setTextColor(QtGui.QColor("black")) self.messages.append(f"[{tstr}]") @@ -178,9 +178,9 @@ def __init__( insType: Optional[str] = None, insName: Optional[str] = None, kwargsStr: Optional[str] = None, - parent=None, - flags=(QtCore.Qt.CustomizeWindowHint | QtCore.Qt.WindowCloseButtonHint), - ): + parent: Optional[QtWidgets.QWidget] = None, + flags: Any = (QtCore.Qt.WindowType.CustomizeWindowHint | QtCore.Qt.WindowType.WindowCloseButtonHint), + ) -> None: super().__init__(parent, flags) tittleText = "Create New Instrument" @@ -212,12 +212,12 @@ def __init__( ) self.acceptButton.setSizePolicy(buttonSizePolicy) layout.addWidget(self.acceptButton) - layout.setAlignment(self.acceptButton, QtCore.Qt.AlignCenter) + layout.setAlignment(self.acceptButton, QtCore.Qt.AlignmentFlag.AlignCenter) self.acceptButton.clicked.connect(self.onAcceptButton) @QtCore.Slot() - def onAcceptButton(self): + def onAcceptButton(self) -> None: insType = self.typeEdit.text() insName = self.nameEdit.text() # Args is not a normal line edit, value evaluates the args and kwargs already @@ -226,7 +226,7 @@ def onAcceptButton(self): @QtCore.Slot(str) -def onExceptionDialog(exception: str): +def onExceptionDialog(exception: str) -> None: """ Opens a dialog displaying an exception. @@ -242,11 +242,11 @@ def onExceptionDialog(exception: str): accept = QtWidgets.QPushButton("Accept") layout.addWidget(exceptionRaisedLabel) - layout.setAlignment(exceptionRaisedLabel, QtCore.Qt.AlignCenter) + layout.setAlignment(exceptionRaisedLabel, QtCore.Qt.AlignmentFlag.AlignCenter) layout.addWidget(exceptionLabel) - layout.setAlignment(exceptionLabel, QtCore.Qt.AlignCenter) + layout.setAlignment(exceptionLabel, QtCore.Qt.AlignmentFlag.AlignCenter) layout.addWidget(accept) - layout.setAlignment(accept, QtCore.Qt.AlignCenter) + layout.setAlignment(accept, QtCore.Qt.AlignmentFlag.AlignCenter) accept.clicked.connect(dialog.accept) dialog.exec_() @@ -258,8 +258,14 @@ class PossibleInstrumentDisplayItem(QtWidgets.QTreeWidgetItem): """ def __init__( - self, text, fullInsType, configName=None, lineEdit=None, *args, **kwargs - ): + self, + text: List[str], + fullInsType: str, + configName: Optional[str] = None, + lineEdit: Optional[QtWidgets.QLineEdit] = None, + *args: Any, + **kwargs: Any, + ) -> None: super().__init__(text, *args, **kwargs) self.configName = configName self.lineEdit = lineEdit @@ -291,7 +297,7 @@ class PossibleInstrumentsDisplay(QtWidgets.QTreeWidget): cols = ["Instrument Type & Preset", "Instrument Name", "Create Instrument"] - def __init__(self, guiConfig: Optional[dict] = None, *args): + def __init__(self, guiConfig: Optional[dict] = None, *args: Any) -> None: super().__init__(*args) self.setColumnCount(len(self.cols)) @@ -308,12 +314,12 @@ def __init__(self, guiConfig: Optional[dict] = None, *args): self.deletePossibleInstrumentAction = QtWidgets.QAction("Delete") self.contextMenu = QtWidgets.QMenu(self) - self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu) self.contextMenu.addAction(self.basedInstrumentAction) self.contextMenu.addSeparator() self.contextMenu.addAction(self.deletePossibleInstrumentAction) self.customContextMenuRequested.connect( - lambda x: self.contextMenu.exec_(self.mapToGlobal(x)) + lambda x: self.contextMenu.exec_(self.mapToGlobal(x)) # type: ignore[arg-type] ) self.basedInstrumentAction.triggered.connect(self.onBasedInstrumentAction) @@ -328,7 +334,7 @@ def __init__(self, guiConfig: Optional[dict] = None, *args): self.expandAll() - def loadConfig(self, config: dict): + def loadConfig(self, config: dict) -> None: for key, value in config.items(): # In the config, the name of the instrument and the config name are the same. self.addInstrumentToTree(value["type"], key, key) @@ -341,14 +347,14 @@ def addInstrumentToTree( fullInsType: str = "InstrumentType", insName: str = "MyInstrument", configName: Optional[str] = None, - ): + ) -> None: """ Each type is grouped together under a parent item of that type. If that parent item does not exist yet it creates it """ insType = fullInsType.split(".")[-1] items = self.findItems( - insType, QtCore.Qt.MatchExactly | QtCore.Qt.MatchExactly, 0 + insType, QtCore.Qt.MatchFlag.MatchExactly | QtCore.Qt.MatchFlag.MatchExactly, 0 # type: ignore[arg-type] ) # Only add the instrument to the tree if there are no other instruments of the same type already @@ -374,7 +380,7 @@ def addInstrumentToTree( lineEdit.returnPressed.connect(lambda: createButton.clicked.emit()) lineEdit.setText(insName) item = PossibleInstrumentDisplayItem( - lst, fullInsType=fullInsType, configName=configName, lineEdit=lineEdit + lst, fullInsType=fullInsType, configName=configName, lineEdit=lineEdit # type: ignore[arg-type] ) parent.addChild(item) @@ -387,18 +393,18 @@ def addInstrumentToTree( ) ) - def onBasedInstrumentAction(self): + def onBasedInstrumentAction(self) -> None: items = self.selectedItems() for item in items: insName = None - if item.lineEdit is not None: - insName = item.lineEdit.text() + if item.lineEdit is not None: # type: ignore[attr-defined] + insName = item.lineEdit.text() # type: ignore[attr-defined] self.basedInstrumentRequested.emit( - item.configName, item.fullInsType, insName + item.configName, item.fullInsType, insName # type: ignore[attr-defined] ) @QtCore.Slot() - def onRemoveInstrumentFromTree(self): + def onRemoveInstrumentFromTree(self) -> None: """ Removes both the potential instrument from the widget and the presets from the config dictionary. """ @@ -406,16 +412,16 @@ def onRemoveInstrumentFromTree(self): for item in items: if item.childCount() == 0: parent = item.parent() - if item.configName is not None and item.configName in self.config: - del self.config[item.configName] - parent.removeChild(item) - if parent.childCount() == 0: + if item.configName is not None and item.configName in self.config: # type: ignore[attr-defined] + del self.config[item.configName] # type: ignore[attr-defined] + parent.removeChild(item) # type: ignore[union-attr] + if parent.childCount() == 0: # type: ignore[union-attr] self.takeTopLevelItem((self.indexOfTopLevelItem(parent))) else: for i in range(item.childCount()): child = item.child(i) - if child.configName in self.config: - del self.config[child.configName] + if child.configName in self.config: # type: ignore[union-attr] + del self.config[child.configName] # type: ignore[union-attr] self.takeTopLevelItem(self.indexOfTopLevelItem(item)) @@ -439,7 +445,9 @@ class InstrumentsCreator(QtWidgets.QWidget): #: Arguments -- The str message of the error/reason as to why it could not create the instrument newInstrumentFailed = QtCore.Signal(object) - def __init__(self, cli: Client, guiConfig: dict, *args, **kwargs): + def __init__( + self, cli: Client, guiConfig: dict, *args: Any, **kwargs: Any + ) -> None: super().__init__(*args, **kwargs) self.guiConfig = guiConfig @@ -476,7 +484,7 @@ def onCreateNewInstrumentClicked( configName: Optional[str] = None, insType: Optional[str] = None, insName: Optional[str] = None, - ): + ) -> None: # go through all the possible arguments in the config and write them in kwarg form kwargsStr = None if configName is not None and configName in self.guiConfig: @@ -499,7 +507,9 @@ def onCreateNewInstrumentClicked( dialog.exec_() @QtCore.Slot(str, str, tuple) - def onDialogNewInstrument(self, insType, insName, argsKwargs): + def onDialogNewInstrument( + self, insType: str, insName: str, argsKwargs: Tuple[Any, Any] + ) -> None: args, kwargs = argsKwargs if args is None: @@ -509,7 +519,9 @@ def onDialogNewInstrument(self, insType, insName, argsKwargs): self.createNewInstrument(insType, insName, *args, **kwargs) @QtCore.Slot(str, str, str) - def onPossibleInstrumentDisplayClicked(self, configName, insType, insName): + def onPossibleInstrumentDisplayClicked( + self, configName: str, insType: str, insName: str + ) -> None: """ Creates new instrument based on a possible instrument. Only creates it if it can find the configName in the config. @@ -543,12 +555,14 @@ def onPossibleInstrumentDisplayClicked(self, configName, insType, insName): "you cannot create instruments that are not in the config from here yet" ) - def createNewInstrument(self, insType, insName, *args, **kwargs): + def createNewInstrument( + self, insType: str, insName: str, *args: Any, **kwargs: Any + ) -> None: if insName in self.cli.list_instruments(): self.newInstrumentFailed.emit(f'Instrument "{insName}" already exists.') return try: - self.cli.find_or_create_instrument( + self.cli.find_or_create_instrument( # type: ignore[misc] name=insName, instrument_class=insType, *args, **kwargs ) self.newInstrumentCreated.emit() @@ -566,7 +580,7 @@ def __init__( startServer: Optional[bool] = True, guiConfig: Optional[dict] = None, **serverKwargs: Any, - ): + ) -> None: super().__init__() self._paramValuesFile = os.path.abspath(os.path.join(".", "parameters.json")) @@ -613,12 +627,12 @@ def __init__( self.stationList.itemDoubleClicked.connect(self.addInstrumentTab) self.stationList.closeRequested.connect(self.closeInstrument) - stationWidgets = QtWidgets.QSplitter(QtCore.Qt.Horizontal) + stationWidgets = QtWidgets.QSplitter(QtCore.Qt.Orientation.Horizontal) stationWidgets.addWidget(self.stationList) stationWidgets.addWidget(self.stationObjInfo) stationWidgets.setSizes([300, 500]) - instrumentsWidgets = QtWidgets.QSplitter(QtCore.Qt.Vertical) + instrumentsWidgets = QtWidgets.QSplitter(QtCore.Qt.Orientation.Vertical) instrumentsWidgets.addWidget(stationWidgets) instrumentsWidgets.addWidget(self.instrumentCreator) @@ -630,31 +644,31 @@ def __init__( # Toolbar. self.toolBar = self.addToolBar("Tools") - self.toolBar.setIconSize(QtCore.QSize(16, 16)) + self.toolBar.setIconSize(QtCore.QSize(16, 16)) # type: ignore[union-attr] # Station tools. - self.toolBar.addWidget(QtWidgets.QLabel("Station:")) + self.toolBar.addWidget(QtWidgets.QLabel("Station:")) # type: ignore[union-attr] self.refreshStationAction = QtWidgets.QAction( QtGui.QIcon(":/icons/refresh.svg"), "Refresh", self ) self.refreshStationAction.triggered.connect(self.refreshStationComponents) - self.toolBar.addAction(self.refreshStationAction) + self.toolBar.addAction(self.refreshStationAction) # type: ignore[union-attr] # Parameter tools. - self.toolBar.addSeparator() - self.toolBar.addWidget(QtWidgets.QLabel("Params:")) + self.toolBar.addSeparator() # type: ignore[union-attr] + self.toolBar.addWidget(QtWidgets.QLabel("Params:")) # type: ignore[union-attr] self.loadParamsAction = QtWidgets.QAction( QtGui.QIcon(":/icons/load.svg"), "Load from file", self ) self.loadParamsAction.triggered.connect(self.loadParamsFromFile) - self.toolBar.addAction(self.loadParamsAction) + self.toolBar.addAction(self.loadParamsAction) # type: ignore[union-attr] self.saveParamsAction = QtWidgets.QAction( QtGui.QIcon(":/icons/save.svg"), "Save to file", self ) self.saveParamsAction.triggered.connect(self.saveParamsToFile) - self.toolBar.addAction(self.saveParamsAction) + self.toolBar.addAction(self.saveParamsAction) # type: ignore[union-attr] self.serverStatus.testButton.clicked.connect( lambda x: self.client.ask("Ping server.") @@ -670,10 +684,10 @@ def __init__( # printSpaceAction.triggered.connect(lambda x: print("\n \n \n \n")) # self.toolBar.addAction(printSpaceAction) - def log(self, message, level=LogLevels.info): + def log(self, message: str, level: LogLevels = LogLevels.info) -> None: log(logger, message, level) - def closeEvent(self, event): + def closeEvent(self, event: Optional[QtGui.QCloseEvent]) -> None: for name, widget in list(self.instrumentTabsOpen.items()): try: widget.close() @@ -695,39 +709,39 @@ def closeEvent(self, event): self.client.disconnect() except Exception: pass - event.accept() + event.accept() # type: ignore[union-attr] - def startServer(self): + def startServer(self) -> None: """Start the instrument server in a separate thread.""" - self.stationServer = StationServer(**self._serverKwargs) - self.stationServerThread = QtCore.QThread() - self.stationServer.moveToThread(self.stationServerThread) - self.stationServerThread.started.connect(self.stationServer.startServer) - self.stationServer.finished.connect(lambda: self.log("ZMQ server closed.")) - self.stationServer.finished.connect(self.stationServerThread.quit) - self.stationServer.finished.connect(self.stationServer.deleteLater) + self.stationServer = StationServer(**self._serverKwargs) # type: ignore[assignment] + self.stationServerThread = QtCore.QThread() # type: ignore[assignment] + self.stationServer.moveToThread(self.stationServerThread) # type: ignore[attr-defined] + self.stationServerThread.started.connect(self.stationServer.startServer) # type: ignore[arg-type,attr-defined] + self.stationServer.finished.connect(lambda: self.log("ZMQ server closed.")) # type: ignore[attr-defined] + self.stationServer.finished.connect(self.stationServerThread.quit) # type: ignore[attr-defined] + self.stationServer.finished.connect(self.stationServer.deleteLater) # type: ignore[attr-defined] # Connecting some additional things for messages. - self.stationServer.serverStarted.connect(self.serverStatus.setListeningAddress) - self.stationServer.serverStarted.connect(self.client.start) - self.stationServer.serverStarted.connect(self.refreshStationComponents) - self.stationServer.finished.connect( + self.stationServer.serverStarted.connect(self.serverStatus.setListeningAddress) # type: ignore[attr-defined] + self.stationServer.serverStarted.connect(self.client.start) # type: ignore[attr-defined] + self.stationServer.serverStarted.connect(self.refreshStationComponents) # type: ignore[attr-defined] + self.stationServer.finished.connect( # type: ignore[attr-defined] lambda: self.log("Server thread finished.", LogLevels.info) ) - self.stationServer.messageReceived.connect(self._messageReceived) - self.stationServer.instrumentCreated.connect(self.addInstrumentToGui) - self.stationServer.funcCalled.connect(self.onFuncCalled) + self.stationServer.messageReceived.connect(self._messageReceived) # type: ignore[attr-defined] + self.stationServer.instrumentCreated.connect(self.addInstrumentToGui) # type: ignore[attr-defined] + self.stationServer.funcCalled.connect(self.onFuncCalled) # type: ignore[attr-defined] - self.stationServerThread.start() + self.stationServerThread.start() # type: ignore[attr-defined] - def getServerIfRunning(self): - if self.stationServer is not None and self.stationServerThread.isRunning(): + def getServerIfRunning(self) -> Optional["StationServer"]: + if self.stationServer is not None and self.stationServerThread.isRunning(): # type: ignore[union-attr] return self.stationServer else: return None @QtCore.Slot(str, str) - def _messageReceived(self, message: str, reply: str): + def _messageReceived(self, message: str, reply: str) -> None: maxLen = 80 messageSummary = message[:maxLen] if len(message) > maxLen: @@ -740,8 +754,11 @@ def _messageReceived(self, message: str, reply: str): self.serverStatus.addMessageAndReply(messageSummary, replySummary) def addInstrumentToGui( - self, instrumentBluePrint: InstrumentModuleBluePrint, insArgs, insKwargs - ): + self, + instrumentBluePrint: InstrumentModuleBluePrint, + insArgs: Any, + insKwargs: Any, + ) -> None: """ Add an instrument to the station list. @@ -770,7 +787,7 @@ def addInstrumentToGui( instrumentBluePrint.instrument_module_class, instrumentBluePrint.name ) - def removeInstrumentFromGui(self, name: str): + def removeInstrumentFromGui(self, name: str) -> None: """Remove an instrument from the station list.""" self.stationList.removeObject(name) del self._bluePrints[name] @@ -778,7 +795,7 @@ def removeInstrumentFromGui(self, name: str): self.tabs.removeTab(self.tabs.indexOf(self.instrumentTabsOpen[name])) del self.instrumentTabsOpen[name] - def refreshStationComponents(self): + def refreshStationComponents(self) -> None: """Clear and re-populate the widget holding the station components, using the objects that are currently registered in the station.""" if getattr(self.client, "_closed", False) or not self.client.connected: @@ -796,7 +813,7 @@ def refreshStationComponents(self): self._bluePrints[ins] = bp self.stationList.resizeColumnToContents(0) - def loadParamsFromFile(self): + def loadParamsFromFile(self) -> None: """Load the values of all parameters present in the server's params json file to parameters registered in the station (incl those in instruments).""" @@ -808,7 +825,7 @@ def loadParamsFromFile(self): except Exception as e: logger.error(f"Loading failed. {type(e)}: {e.args}") - def saveParamsToFile(self): + def saveParamsToFile(self) -> None: """Save the values of all parameters registered in the station (incl those in instruments) to the server's param json file.""" @@ -821,15 +838,17 @@ def saveParamsToFile(self): logger.error(f"Saving failed. {type(e)}: {e.args}") @QtCore.Slot(str) - def displayComponentInfo(self, name: Union[str, None]): + def displayComponentInfo(self, name: Union[str, None]) -> None: if name is not None and name in self._bluePrints: bp = self._bluePrints[name] else: bp = None - self.stationObjInfo.setObject(bp) + self.stationObjInfo.setObject(bp) # type: ignore[arg-type] @QtCore.Slot(QtWidgets.QTreeWidgetItem, int) - def addInstrumentTab(self, item: QtWidgets.QTreeWidgetItem, index: int): + def addInstrumentTab( + self, item: QtWidgets.QTreeWidgetItem, index: int + ) -> None: """ Gets called when the user double clicks and item of the instrument list. Adds a new generic instrument GUI window to the tab bar. @@ -854,7 +873,7 @@ def addInstrumentTab(self, item: QtWidgets.QTreeWidgetItem, index: int): if "kwargs" in self._guiConfig[name]["gui"]: kwargs = self._guiConfig[name]["gui"]["kwargs"] - kwargs["sub_port"] = kwargs.get("sub_port", self.stationServer.port + 1) + kwargs["sub_port"] = kwargs.get("sub_port", self.stationServer.port + 1) # type: ignore[union-attr] insWidget = widgetClass(ins, parent=self, **kwargs) index = self.tabs.addTab(insWidget, ins.name) self.instrumentTabsOpen[ins.name] = insWidget @@ -869,13 +888,15 @@ def onTabDeleted(self, name: str) -> None: del self.instrumentTabsOpen[name] @QtCore.Slot(str, object, object, object) - def onFuncCalled(self, n, args, kw, ret): + def onFuncCalled( + self, n: str, args: Any, kw: Any, ret: Any + ) -> None: if n == "close_and_remove_instrument": for ins in args: self.removeInstrumentFromGui(ins) @QtCore.Slot(str) - def closeInstrument(self, ins): + def closeInstrument(self, ins: str) -> None: if ins in self.client.list_instruments(): self.client.close_instrument(ins) @@ -883,7 +904,7 @@ def closeInstrument(self, ins): class DetachedServerGui(QtWidgets.QMainWindow): """A detached version of the server gui.""" - def __init__(self, host: str = "localhost", port: int = 5555): + def __init__(self, host: str = "localhost", port: int = 5555) -> None: super().__init__() self.instrumentTabsOpen: dict[str, GenericInstrument] = {} @@ -903,7 +924,7 @@ def __init__(self, host: str = "localhost", port: int = 5555): self.stationList.componentSelected.connect(self.displayComponentInfo) self.stationList.itemDoubleClicked.connect(self.addInstrumentTab) - stationWidgets = QtWidgets.QSplitter(QtCore.Qt.Horizontal) + stationWidgets = QtWidgets.QSplitter(QtCore.Qt.Orientation.Horizontal) stationWidgets.addWidget(self.stationList) stationWidgets.addWidget(self.stationObjInfo) stationWidgets.setSizes([300, 500]) @@ -912,19 +933,19 @@ def __init__(self, host: str = "localhost", port: int = 5555): # Toolbar. self.toolBar = self.addToolBar("Tools") - self.toolBar.setIconSize(QtCore.QSize(16, 16)) + self.toolBar.setIconSize(QtCore.QSize(16, 16)) # type: ignore[union-attr] # Station tools. - self.toolBar.addWidget(QtWidgets.QLabel("Station:")) + self.toolBar.addWidget(QtWidgets.QLabel("Station:")) # type: ignore[union-attr] self.refreshStationAction = QtWidgets.QAction( QtGui.QIcon(":/icons/refresh.svg"), "Refresh", self ) self.refreshStationAction.triggered.connect(self.refreshStationComponents) - self.toolBar.addAction(self.refreshStationAction) + self.toolBar.addAction(self.refreshStationAction) # type: ignore[union-attr] self.refreshStationComponents() - def refreshStationComponents(self): + def refreshStationComponents(self) -> None: """Clear and re-populate the widget holding the station components, using the objects that are currently registered in the station.""" self.stationList.clear() @@ -934,12 +955,14 @@ def refreshStationComponents(self): self.stationList.resizeColumnToContents(0) @QtCore.Slot(str) - def displayComponentInfo(self, name: Union[str, None]): + def displayComponentInfo(self, name: Union[str, None]) -> None: if name is not None and name in self.client.list_instruments(): self.stationObjInfo.setObject(self.client.getBluePrint(name)) @QtCore.Slot(QtWidgets.QTreeWidgetItem, int) - def addInstrumentTab(self, item: QtWidgets.QTreeWidgetItem, index: int): + def addInstrumentTab( + self, item: QtWidgets.QTreeWidgetItem, index: int + ) -> None: name = item.text(0) if name not in self.instrumentTabsOpen: ins = self.client.find_or_create_instrument(name) @@ -987,21 +1010,21 @@ class EmbeddedClient(QtClient): inside the server application.""" @QtCore.Slot(str) - def start(self, addr: str): + def start(self, addr: str) -> None: if self._closed: return self.addr = "tcp://localhost:" + addr.split(":")[-1] self.connect() @QtCore.Slot(str) - def ask(self, msg: str): + def ask(self, msg: str) -> Any: logger.debug(f"Test client sending request: {msg}") reply = super().ask(msg) logger.debug(f"Test client received reply: {reply}") return reply -def bluePrintToHtml(bp: Union[ParameterBluePrint, InstrumentModuleBluePrint]): +def bluePrintToHtml(bp: Union[ParameterBluePrint, InstrumentModuleBluePrint]) -> str: header = f""" @@ -1019,7 +1042,7 @@ def bluePrintToHtml(bp: Union[ParameterBluePrint, InstrumentModuleBluePrint]): return header + instrumentToHtml(bp) + footer -def parameterToHtml(bp: ParameterBluePrint, headerLevel=None): +def parameterToHtml(bp: ParameterBluePrint, headerLevel: Optional[int] = None) -> str: setget = [] setgetstr = "" if bp.gettable: @@ -1048,7 +1071,7 @@ def parameterToHtml(bp: ParameterBluePrint, headerLevel=None): return ret + var -def instrumentToHtml(bp: InstrumentModuleBluePrint): +def instrumentToHtml(bp: InstrumentModuleBluePrint) -> str: ret = f"""
{bp.name}
    diff --git a/src/instrumentserver/server/core.py b/src/instrumentserver/server/core.py index 3f196cc..7ed9233 100644 --- a/src/instrumentserver/server/core.py +++ b/src/instrumentserver/server/core.py @@ -178,7 +178,7 @@ def __init__( ) # a queue for responses that are ready to be sent to client - self._response_queue = queue.Queue() + self._response_queue: queue.Queue = queue.Queue() # a socket pair for immediate wakeup of the main thread that sends response to client self._wakeup_r, self._wakeup_w = socket.socketpair() self._wakeup_r.setblocking(False) @@ -188,7 +188,7 @@ def __init__( self._instrument_locks: dict[str, threading.RLock] = {} self._instrument_locks_lock = threading.Lock() - def _runInitScript(self): + def _runInitScript(self) -> None: if os.path.exists(self.initScript): path = os.path.abspath(self.initScript) env = dict(station=self.station) @@ -303,7 +303,7 @@ def startServer(self) -> bool: logger.info("StationServer shut down cleanly.") return True - def _handleRouterMessage(self, identity, message): + def _handleRouterMessage(self, identity: bytes, message: Any) -> None: """ Handle a router message and put the response message in the response queue. @@ -376,7 +376,7 @@ def _handleRouterMessage(self, identity, message): def executeServerInstruction( self, instruction: ServerInstruction - ) -> Tuple[ServerResponse, str]: + ) -> ServerResponse: """ This is the interpreter function that the server will call to translate the dictionary received from the proxy to instrument calls. @@ -449,10 +449,10 @@ def _createInstrument(self, spec: InstrumentCreationSpec) -> None: if lock is None: # in case name isn't in station yet, just guard creation with the dict lock lock = ( - self._instrument_locks_lock + self._instrument_locks_lock # type: ignore[assignment] ) # coarse but fine for this rare operation - with lock: + with lock: # type: ignore[union-attr] new_instrument = qc.find_or_create_instrument( cls, spec.name, *args, **kwargs ) @@ -472,7 +472,7 @@ def _callObject(self, spec: CallSpec) -> Any: args = spec.args if spec.args is not None else [] kwargs = spec.kwargs if spec.kwargs is not None else {} - def _invoke(): + def _invoke() -> Any: ret = obj(*args, **kwargs) # Check if a new parameter is being created. @@ -516,14 +516,14 @@ def _getBluePrint( logger.debug(f"Fetching blueprint for: {path}") obj = nestedAttributeFromString(self.station, path) if isinstance(obj, tuple(INSTRUMENT_MODULE_BASE_CLASSES)): - instrument_blueprint = bluePrintFromInstrumentModule(path, obj) + instrument_blueprint = bluePrintFromInstrumentModule(path, obj) # type: ignore[arg-type] if instrument_blueprint is None: raise ValueError( f"Failed to create blueprint for instrument module {path}" ) return instrument_blueprint elif isinstance(obj, tuple(PARAMETER_BASE_CLASSES)): - parameter_blueprint = bluePrintFromParameter(path, obj) + parameter_blueprint = bluePrintFromParameter(path, obj) # type: ignore[arg-type] if parameter_blueprint is None: raise ValueError(f"Failed to create blueprint for parameter {path}") return parameter_blueprint @@ -548,7 +548,7 @@ def _toParamDict(self, opts: ParameterSerializeSpec) -> Dict[str, Any]: kwargs.update(includeMeta=includeMeta) return serialize.toParamDict(obj, *args, **kwargs) - def _fromParamDict(self, params: Dict[str, Any]): + def _fromParamDict(self, params: Dict[str, Any]) -> None: return serialize.fromParamDict(params, self.station) def _getGuiConfig(self, instrumentName: str) -> str: @@ -567,7 +567,7 @@ def _getGuiConfig(self, instrumentName: str) -> str: return json.dumps(self.guiConfig[instrumentName]) - def _broadcastParameterChange(self, blueprint: ParameterBroadcastBluePrint): + def _broadcastParameterChange(self, blueprint: ParameterBroadcastBluePrint) -> None: """ Broadcast any changes to parameters in the server. The message is composed of a 2 part array. The first item is the name of the instrument the parameter is from, @@ -576,17 +576,19 @@ def _broadcastParameterChange(self, blueprint: ParameterBroadcastBluePrint): :param blueprint: The parameter broadcast blueprint that is being broadcast """ - sendBroadcast(self.broadcastSocket, blueprint.name.split(".")[0], blueprint) + sendBroadcast(self.broadcastSocket, blueprint.name.split(".")[0], blueprint) # type: ignore[arg-type] if self.externalBroadcastAddr is not None: sendBroadcast( - self.externalBroadcastSocket, blueprint.name.split(".")[0], blueprint + self.externalBroadcastSocket, blueprint.name.split(".")[0], blueprint # type: ignore[arg-type] ) logger.info( f"Parameter {blueprint.name} has broadcast an update of type: {blueprint.action}," f" with a value: {blueprint.value}." ) - def _newOrDeleteParameterDetection(self, spec, args, kwargs): + def _newOrDeleteParameterDetection( + self, spec: CallSpec, args: List[Any], kwargs: Dict[str, Any] + ) -> None: """ Detects if the call action is being used to create a new parameter or deletes an existing parameter. If so, it creates the parameter broadcast blueprint and broadcast it. @@ -597,13 +599,13 @@ def _newOrDeleteParameterDetection(self, spec, args, kwargs): """ if spec.target.split(".")[-1] == "add_parameter": - name = spec.target.split(".")[0] + "." + ".".join(spec.args) + name = spec.target.split(".")[0] + "." + ".".join(spec.args) # type: ignore[arg-type] pb = ParameterBroadcastBluePrint( name, "parameter-creation", kwargs["initial_value"], kwargs["unit"] ) self._broadcastParameterChange(pb) elif spec.target.split(".")[-1] == "remove_parameter": - name = spec.target.split(".")[0] + "." + ".".join(spec.args) + name = spec.target.split(".")[0] + "." + ".".join(spec.args) # type: ignore[arg-type] pb = ParameterBroadcastBluePrint(name, "parameter-deletion") self._broadcastParameterChange(pb) @@ -663,6 +665,6 @@ def startServer( thread = QtCore.QThread() server.moveToThread(thread) server.finished.connect(thread.quit) - thread.started.connect(server.startServer) + thread.started.connect(server.startServer) # type: ignore[arg-type] thread.start() return server, thread diff --git a/src/instrumentserver/server/pollingWorker.py b/src/instrumentserver/server/pollingWorker.py index 5b81de5..2e97fad 100644 --- a/src/instrumentserver/server/pollingWorker.py +++ b/src/instrumentserver/server/pollingWorker.py @@ -18,7 +18,7 @@ def __init__(self, pollingRates: Optional[Dict[str, int]] = None): self.pollingRates = pollingRates # Used by the qtimers, get value of the param - def getParamValue(self, paramName): + def getParamValue(self, paramName: str) -> None: try: parts = paramName.split(".") instr = self.cli.find_or_create_instrument(parts[0]) @@ -32,28 +32,28 @@ def getParamValue(self, paramName): # Don't re-raise the exception, just log it and continue # Creates a qtimer for each param in the dict with the interval specified - def run(self): + def run(self) -> None: timers = [] # Deletes param from dict if it does not exist delList = [] - for param in self.pollingRates: + for param in self.pollingRates: # type: ignore[union-attr] if param not in self.cli.getParamDict(param.split(".")[0]): logger.warning(f"Parameter {param} does not exist") delList.append(param) for item in delList: - del self.pollingRates[item] + del self.pollingRates[item] # type: ignore[union-attr] # Prints which parameters are being polled logger.info( - f"Broadcasting the following parameters: {list(self.pollingRates.keys())}" + f"Broadcasting the following parameters: {list(self.pollingRates.keys())}" # type: ignore[union-attr] ) # Creates timers for each param in the dict - for param in self.pollingRates: + for param in self.pollingRates: # type: ignore[union-attr] timer = QtCore.QTimer() timer.timeout.connect(lambda name=param: self.getParamValue(name)) - timer.start(int(self.pollingRates.get(param) * 1000)) + timer.start(int(self.pollingRates.get(param) * 1000)) # type: ignore[union-attr,operator] timers.append(timer) self.exec_() diff --git a/src/instrumentserver/testing/dummy_instruments/rf.py b/src/instrumentserver/testing/dummy_instruments/rf.py index 7f078eb..06564d1 100644 --- a/src/instrumentserver/testing/dummy_instruments/rf.py +++ b/src/instrumentserver/testing/dummy_instruments/rf.py @@ -1,9 +1,10 @@ +from typing import Any + import numpy as np +from numpy.typing import NDArray from qcodes import Instrument, ParameterWithSetpoints, find_or_create_instrument from qcodes.utils import validators -from scipy import ( - constants, # type: ignore[import-untyped] # We don't need mypy checks for this dependency that is only used here. -) +from scipy import constants class ResonatorResponse(Instrument): @@ -14,7 +15,7 @@ class ResonatorResponse(Instrument): properties added as parameters. """ - def __init__(self, name, f0=5e9, df=1e6, **kw): + def __init__(self, name: str, f0: float = 5e9, df: float = 1e6, **kw: Any) -> None: super().__init__(name, **kw) self._frq_mod = 0.0 @@ -104,7 +105,7 @@ def __init__(self, name, f0=5e9, df=1e6, **kw): get_cmd=self._get_data, ) - def modulate_frequency(self, delta: float = 0, multiply=False) -> None: + def modulate_frequency(self, delta: float = 0, multiply: bool = False) -> None: """Add an offset to the resonance frequency. If `multiply` is ``True``, the change in frequency is the product of `delta` @@ -114,12 +115,12 @@ def modulate_frequency(self, delta: float = 0, multiply=False) -> None: self._frq_mod_multiply = multiply # private utility methods - def _frequency_vals(self): + def _frequency_vals(self) -> "NDArray[np.floating]": return np.linspace( self.start_frequency(), self.stop_frequency(), self.npoints() ) - def _get_data(self): + def _get_data(self) -> "NDArray[np.complexfloating]": f0 = self.resonator_frequency() if self._frq_mod_multiply: f0 *= self._frq_mod @@ -138,7 +139,15 @@ def _get_data(self): return data - def _resonator_reflection_signal(self, fvals, f0, df, P_in, BW, T_N): + def _resonator_reflection_signal( + self, + fvals: "NDArray[np.floating]", + f0: float, + df: float, + P_in: float, + BW: float, + T_N: float, + ) -> "NDArray[np.complexfloating]": """Compute a realistic resonator reflection signal of a one-port resonator, including random noise. @@ -163,7 +172,7 @@ def _resonator_reflection_signal(self, fvals, f0, df, P_in, BW, T_N): class Generator(Instrument): """A simple dummy that mocks an RF generator.""" - def __init__(self, name, *arg, **kw): + def __init__(self, name: str, *arg: Any, **kw: Any) -> None: super().__init__(name, *arg, **kw) self.add_parameter( @@ -191,7 +200,9 @@ class FluxControl(Instrument): """A dummy that hooks to :class:`.ResonatorResponse` and modifies its resonance frequency as if the resonator were a squid.""" - def __init__(self, name: str, resonator_instrument: str, *args, **kwargs): + def __init__( + self, name: str, resonator_instrument: str, *args: Any, **kwargs: Any + ) -> None: super().__init__(name, *args, **kwargs) self._resonator = find_or_create_instrument( @@ -213,7 +224,7 @@ def __init__(self, name: str, resonator_instrument: str, *args, **kwargs): initial_value=0, ) - def _set_flux(self, flux): + def _set_flux(self, flux: float) -> None: mod = 1.0 / ( 1.0 + self.inductive_participation_ratio() / np.abs(np.cos(np.pi * flux)) ) From 32be17fbe6e90efb728fd0dff5b5f8b8bb6101cf Mon Sep 17 00:00:00 2001 From: marcosf2 Date: Thu, 30 Apr 2026 16:02:54 -0500 Subject: [PATCH 08/12] Refactor imports for readability and remove unused variable in `pprint` method --- .github/workflows/ci.yml | 57 ++++++++++++++++++++++++++++ src/instrumentserver/blueprints.py | 3 -- src/instrumentserver/gui/__init__.py | 7 +++- 3 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1cc99e4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,57 @@ +name: CI + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + - name: Install dependencies + run: uv sync --frozen --group dev + - name: Ruff + run: uv run ruff check src/ + - name: Mypy + run: uv run mypy + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + - name: Install system deps for PyQt5 + run: | + sudo apt-get update + sudo apt-get install -y \ + libgl1 \ + libegl1 \ + libxkbcommon0 \ + libdbus-1-3 \ + libxcb-icccm4 \ + libxcb-image0 \ + libxcb-keysyms1 \ + libxcb-randr0 \ + libxcb-render-util0 \ + libxcb-shape0 \ + libxcb-sync1 \ + libxcb-xfixes0 \ + libxcb-xinerama0 \ + libxcb-xkb1 \ + xvfb + - name: Install dependencies + run: uv sync --frozen --group dev + - name: Pytest + run: xvfb-run -a uv run pytest -v diff --git a/src/instrumentserver/blueprints.py b/src/instrumentserver/blueprints.py index 61e28fd..94fb288 100644 --- a/src/instrumentserver/blueprints.py +++ b/src/instrumentserver/blueprints.py @@ -377,14 +377,11 @@ def __repr__(self) -> str: return str(self) def pprint(self, indent: int = 0) -> str: - i = indent * " " - bp_type = self.bp_type # type: ignore[attr-defined] ret = f"""name: {self.name} {i}- action: {self.action} {i}- value: {self.value} {i}- unit: {self.unit} -{i}- bp_type: {bp_type} """ return ret diff --git a/src/instrumentserver/gui/__init__.py b/src/instrumentserver/gui/__init__.py index 4602f95..56e8fe3 100644 --- a/src/instrumentserver/gui/__init__.py +++ b/src/instrumentserver/gui/__init__.py @@ -1,7 +1,10 @@ from typing import Optional -from .. import QtCore, QtWidgets -from .. import resource # noqa: F401 +from .. import ( + QtCore, + QtWidgets, + resource, # noqa: F401 +) def getStyleSheet() -> Optional[str]: From d1fbd33b5b2a3f942e545cad1e1243772a56209a Mon Sep 17 00:00:00 2001 From: marcosf2 Date: Thu, 30 Apr 2026 16:11:48 -0500 Subject: [PATCH 09/12] Changes to the ci.yml file --- .github/workflows/ci.yml | 84 +++++++++++++++++++++++++++++----------- 1 file changed, 62 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1cc99e4..63c3e39 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,37 +1,68 @@ -name: CI +name: Code quality on: - push: - branches: - - master pull_request: - branches: - - master + types: [opened, synchronize, reopened] + +permissions: + contents: read jobs: + lockfile: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + python-version: "3.12" + - run: uv lock --locked + lint: runs-on: ubuntu-latest + needs: lockfile + steps: + - uses: actions/checkout@v5 + - uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + python-version: "3.12" + - run: uv sync --locked --group dev + - run: uv run ruff check + + format: + runs-on: ubuntu-latest + needs: lockfile steps: - - uses: actions/checkout@v4 - - name: Set up uv - uses: astral-sh/setup-uv@v3 + - uses: actions/checkout@v5 + - uses: astral-sh/setup-uv@v7 with: enable-cache: true - - name: Install dependencies - run: uv sync --frozen --group dev - - name: Ruff - run: uv run ruff check src/ - - name: Mypy - run: uv run mypy + python-version: "3.12" + - run: uv sync --locked --group dev + - run: uv run ruff format --check + + typecheck: + runs-on: ubuntu-latest + needs: lockfile + steps: + - uses: actions/checkout@v5 + - uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + python-version: "3.12" + - run: uv sync --locked --group dev + - run: uv run mypy test: runs-on: ubuntu-latest + needs: lockfile steps: - - uses: actions/checkout@v4 - - name: Set up uv - uses: astral-sh/setup-uv@v3 + - uses: actions/checkout@v5 + - uses: astral-sh/setup-uv@v7 with: enable-cache: true + python-version: "3.12" - name: Install system deps for PyQt5 run: | sudo apt-get update @@ -51,7 +82,16 @@ jobs: libxcb-xinerama0 \ libxcb-xkb1 \ xvfb - - name: Install dependencies - run: uv sync --frozen --group dev - - name: Pytest - run: xvfb-run -a uv run pytest -v + - run: uv sync --locked --group dev + - run: xvfb-run -a uv run pytest test/ -v + + build: + runs-on: ubuntu-latest + needs: [lint, format, typecheck, test] + steps: + - uses: actions/checkout@v5 + - uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + python-version: "3.12" + - run: uv build From 4eae8700ca3f22f96411046d582160a8b19a4638 Mon Sep 17 00:00:00 2001 From: marcosf2 Date: Thu, 30 Apr 2026 16:14:36 -0500 Subject: [PATCH 10/12] Update `ci.yml` to set `QT_QPA_PLATFORM` and streamline test dependencies --- .github/workflows/ci.yml | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63c3e39..40d4201 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,6 +57,8 @@ jobs: test: runs-on: ubuntu-latest needs: lockfile + env: + QT_QPA_PLATFORM: offscreen steps: - uses: actions/checkout@v5 - uses: astral-sh/setup-uv@v7 @@ -70,20 +72,9 @@ jobs: libgl1 \ libegl1 \ libxkbcommon0 \ - libdbus-1-3 \ - libxcb-icccm4 \ - libxcb-image0 \ - libxcb-keysyms1 \ - libxcb-randr0 \ - libxcb-render-util0 \ - libxcb-shape0 \ - libxcb-sync1 \ - libxcb-xfixes0 \ - libxcb-xinerama0 \ - libxcb-xkb1 \ - xvfb + libdbus-1-3 - run: uv sync --locked --group dev - - run: xvfb-run -a uv run pytest test/ -v + - run: uv run pytest test/ -v build: runs-on: ubuntu-latest From 767255c8acb9fd46d9c2c07e38c8ba8020ea26ea Mon Sep 17 00:00:00 2001 From: marcosf2 Date: Thu, 30 Apr 2026 16:14:59 -0500 Subject: [PATCH 11/12] More formatting --- src/instrumentserver/client/application.py | 19 ++++++---- src/instrumentserver/client/core.py | 4 +- src/instrumentserver/client/proxy.py | 4 +- src/instrumentserver/gui/__init__.py | 4 +- src/instrumentserver/gui/base_instrument.py | 11 ++++-- src/instrumentserver/gui/instruments.py | 26 ++++++++++--- src/instrumentserver/gui/misc.py | 13 +++++-- src/instrumentserver/log.py | 8 ++-- src/instrumentserver/server/application.py | 38 ++++++++++--------- src/instrumentserver/server/core.py | 4 +- test/notebooks/Autoupdate.ipynb | 5 +-- .../Prototype the ParamManager.ipynb | 1 - test/pytest/test_client_station.py | 1 - 13 files changed, 84 insertions(+), 54 deletions(-) diff --git a/src/instrumentserver/client/application.py b/src/instrumentserver/client/application.py index 7557220..be4248e 100644 --- a/src/instrumentserver/client/application.py +++ b/src/instrumentserver/client/application.py @@ -38,8 +38,12 @@ def __init__( form_layout = QtWidgets.QFormLayout(form) form_layout.setContentsMargins(0, 0, 0, 0) form_layout.setSpacing(8) - form_layout.setLabelAlignment(QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter) # type: ignore[arg-type] - form_layout.setFieldGrowthPolicy(QtWidgets.QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) + form_layout.setLabelAlignment( + QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter + ) # type: ignore[arg-type] + form_layout.setFieldGrowthPolicy( + QtWidgets.QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow + ) # Non-editable self.host = QtWidgets.QLineEdit(self.client_station._host) @@ -179,9 +183,7 @@ def listenerEvent(self, message: ParameterBroadcastBluePrint) -> None: if message.action == "parameter-update": logger.info(f"{message.action}: {message.name}: {message.value}") - def openInstrumentTab( - self, item: QtWidgets.QTreeWidgetItem, index: int - ) -> None: + def openInstrumentTab(self, item: QtWidgets.QTreeWidgetItem, index: int) -> None: """ Gets called when the user double clicks and item of the instrument list. Adds a new generic instrument GUI window to the tab bar. @@ -256,7 +258,9 @@ def addParameterLoadSaveToolbar(self) -> None: # --- toolbar basics --- self.toolBar = QtWidgets.QToolBar("Params", self) self.toolBar.setIconSize(QtCore.QSize(22, 22)) - self.toolBar.setToolButtonStyle(QtCore.Qt.ToolButtonStyle.ToolButtonTextBesideIcon) + self.toolBar.setToolButtonStyle( + QtCore.Qt.ToolButtonStyle.ToolButtonTextBesideIcon + ) self.addToolBar(self.toolBar) # --- composite path widget @@ -339,7 +343,8 @@ def loadParams(self) -> None: for i in range(self.tabs.count()): widget = self.tabs.widget(i) if hasattr(widget, "parametersList") and hasattr( - widget.parametersList, "model" # type: ignore[union-attr] + widget.parametersList, + "model", # type: ignore[union-attr] ): widget.parametersList.model.refreshAll() # type: ignore[union-attr] diff --git a/src/instrumentserver/client/core.py b/src/instrumentserver/client/core.py index a96c913..e55a980 100644 --- a/src/instrumentserver/client/core.py +++ b/src/instrumentserver/client/core.py @@ -156,9 +156,7 @@ def disconnect(self) -> None: self.connected = False -def sendRequest( - message: Any, host: str = "localhost", port: int = DEFAULT_PORT -) -> Any: +def sendRequest(message: Any, host: str = "localhost", port: int = DEFAULT_PORT) -> Any: with BaseClient(host, port) as cli: ret = cli.ask(message) return ret diff --git a/src/instrumentserver/client/proxy.py b/src/instrumentserver/client/proxy.py index 0262397..3c43d74 100644 --- a/src/instrumentserver/client/proxy.py +++ b/src/instrumentserver/client/proxy.py @@ -766,9 +766,7 @@ def disconnect(self) -> None: class _QtAdapter(QtCore.QObject): - def __init__( - self, parent: Optional[QtCore.QObject], *arg: Any, **kw: Any - ) -> None: + def __init__(self, parent: Optional[QtCore.QObject], *arg: Any, **kw: Any) -> None: super().__init__(parent) diff --git a/src/instrumentserver/gui/__init__.py b/src/instrumentserver/gui/__init__.py index 56e8fe3..7a20358 100644 --- a/src/instrumentserver/gui/__init__.py +++ b/src/instrumentserver/gui/__init__.py @@ -9,7 +9,9 @@ def getStyleSheet() -> Optional[str]: f = QtCore.QFile(":/style.css") - if f.open(QtCore.QIODevice.OpenModeFlag.ReadOnly | QtCore.QIODevice.OpenModeFlag.Text): # type: ignore[call-overload] + if f.open( + QtCore.QIODevice.OpenModeFlag.ReadOnly | QtCore.QIODevice.OpenModeFlag.Text + ): # type: ignore[call-overload] style = f.readAll() f.close() return str(style, "utf-8") diff --git a/src/instrumentserver/gui/base_instrument.py b/src/instrumentserver/gui/base_instrument.py index 9738ee4..8e5996b 100644 --- a/src/instrumentserver/gui/base_instrument.py +++ b/src/instrumentserver/gui/base_instrument.py @@ -312,7 +312,9 @@ def addItem(self, fullName: str, **kwargs: Any) -> "ItemBase": smName = smName + f".{sm}" items = self.findItems( - smName, QtCore.Qt.MatchFlag.MatchExactly | QtCore.Qt.MatchFlag.MatchRecursive, 0 # type: ignore[arg-type] + smName, + QtCore.Qt.MatchFlag.MatchExactly | QtCore.Qt.MatchFlag.MatchRecursive, + 0, # type: ignore[arg-type] ) if len(items) == 0: @@ -344,7 +346,9 @@ def addItem(self, fullName: str, **kwargs: Any) -> "ItemBase": def removeItem(self, fullName: str) -> None: items = self.findItems( - fullName, QtCore.Qt.MatchFlag.MatchExactly | QtCore.Qt.MatchFlag.MatchRecursive, 0 # type: ignore[arg-type] + fullName, + QtCore.Qt.MatchFlag.MatchExactly | QtCore.Qt.MatchFlag.MatchRecursive, + 0, # type: ignore[arg-type] ) if len(items) > 0: @@ -627,7 +631,8 @@ def restoreCollapsedDict(self) -> None: for x in self.delegateColumns # type: ignore[union-attr] ] proxyDelegateIndexes = [ - self.model().mapFromSource(index) for index in delegateIndexes # type: ignore[union-attr] + self.model().mapFromSource(index) + for index in delegateIndexes # type: ignore[union-attr] ] for delegateIndex in proxyDelegateIndexes: self.openPersistentEditor(delegateIndex) diff --git a/src/instrumentserver/gui/instruments.py b/src/instrumentserver/gui/instruments.py index a2a0fe8..bca6567 100644 --- a/src/instrumentserver/gui/instruments.py +++ b/src/instrumentserver/gui/instruments.py @@ -53,19 +53,25 @@ def __init__( self.nameEdit = QtWidgets.QLineEdit(self) lbl = QtWidgets.QLabel("Name:") - lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter) # type: ignore[arg-type] + lbl.setAlignment( + QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter + ) # type: ignore[arg-type] layout.addWidget(lbl, 0, 0) layout.addWidget(self.nameEdit, 0, 1) self.valueEdit = QtWidgets.QLineEdit(self) lbl = QtWidgets.QLabel("Value:") - lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter) # type: ignore[arg-type] + lbl.setAlignment( + QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter + ) # type: ignore[arg-type] layout.addWidget(lbl, 0, 2) layout.addWidget(self.valueEdit, 0, 3) self.unitEdit = QtWidgets.QLineEdit(self) lbl = QtWidgets.QLabel("Unit:") - lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter) # type: ignore[arg-type] + lbl.setAlignment( + QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter + ) # type: ignore[arg-type] layout.addWidget(lbl, 0, 4) layout.addWidget(self.unitEdit, 0, 5) @@ -80,7 +86,10 @@ def __init__( str(parameterTypes[ParameterTypes.numeric]["name"]) ) lbl = QtWidgets.QLabel("Type:") - lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter) # type: ignore[arg-type] + lbl.setAlignment( + QtCore.Qt.AlignmentFlag.AlignRight + | QtCore.Qt.AlignmentFlag.AlignVCenter + ) # type: ignore[arg-type] layout.addWidget(lbl, 1, 0) layout.addWidget(self.typeSelect, 1, 1) @@ -94,7 +103,10 @@ def __init__( " - 'String': min_length=0, max_length=1e9\n" "See qcodes.utils.validators for details." ) - lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter) # type: ignore[arg-type] + lbl.setAlignment( + QtCore.Qt.AlignmentFlag.AlignRight + | QtCore.Qt.AlignmentFlag.AlignVCenter + ) # type: ignore[arg-type] layout.addWidget(lbl, 1, 2) layout.addWidget(self.valsArgsEdit, 1, 3) @@ -335,7 +347,9 @@ def updateParameter(self, bp: ParameterBroadcastBluePrint) -> None: elif bp.action == "parameter-update" or bp.action == "parameter-call": item = self.findItems( - fullName, QtCore.Qt.MatchFlag.MatchExactly | QtCore.Qt.MatchFlag.MatchRecursive, 0 # type: ignore[arg-type] + fullName, + QtCore.Qt.MatchFlag.MatchExactly | QtCore.Qt.MatchFlag.MatchRecursive, + 0, # type: ignore[arg-type] ) if len(item) == 0: if fullName not in self.itemsHide: # type: ignore[operator] diff --git a/src/instrumentserver/gui/misc.py b/src/instrumentserver/gui/misc.py index 65967ec..1b5ceeb 100644 --- a/src/instrumentserver/gui/misc.py +++ b/src/instrumentserver/gui/misc.py @@ -11,7 +11,9 @@ def __init__( ): super().__init__(parent) - self.setAlignment(QtCore.Qt.AlignmentFlag.AlignVCenter | QtCore.Qt.AlignmentFlag.AlignHCenter) # type: ignore[arg-type] + self.setAlignment( + QtCore.Qt.AlignmentFlag.AlignVCenter | QtCore.Qt.AlignmentFlag.AlignHCenter + ) # type: ignore[arg-type] self._pixmapSize = pixmapSize pix = QtGui.QIcon(":/icons/no-alert.svg").pixmap(*pixmapSize) self.setPixmap(pix) @@ -132,7 +134,9 @@ def mouseMoveEvent(self, a0: QtGui.QMouseEvent) -> None: # type: ignore[overrid painter.end() drag.setPixmap(targetPixmap) - dropAction = drag.exec_(QtCore.Qt.DropAction.MoveAction | QtCore.Qt.DropAction.CopyAction) # type: ignore[call-overload] + dropAction = drag.exec_( + QtCore.Qt.DropAction.MoveAction | QtCore.Qt.DropAction.CopyAction + ) # type: ignore[call-overload] # In linux the drag.exec_ does not return MoveAction, so it must be set manually. if self.dragDroppedPos.x() != 0 and self.dragDroppedPos.y() != 0: @@ -275,7 +279,10 @@ class BaseDialog(QtWidgets.QDialog): def __init__( self, parent: Optional[QtWidgets.QWidget] = None, - flags: Any = (QtCore.Qt.WindowType.CustomizeWindowHint | QtCore.Qt.WindowType.WindowCloseButtonHint), + flags: Any = ( + QtCore.Qt.WindowType.CustomizeWindowHint + | QtCore.Qt.WindowType.WindowCloseButtonHint + ), tittleBarButtonsWidth: int = 108, ) -> None: super().__init__(parent, flags=flags) diff --git a/src/instrumentserver/log.py b/src/instrumentserver/log.py index 05cdefc..2c0b5ca 100644 --- a/src/instrumentserver/log.py +++ b/src/instrumentserver/log.py @@ -38,7 +38,9 @@ def __init__(self, parent: Optional[QtWidgets.QWidget]) -> None: self.widget = QtWidgets.QTextEdit(parent) self.widget.setReadOnly(True) - self._transform: Optional[Callable[[logging.LogRecord, str], Optional[str]]] = None + self._transform: Optional[Callable[[logging.LogRecord, str], Optional[str]]] = ( + None + ) # connect signal to slot that actually touches the widget (GUI thread) self.new_html.connect(self._append_html) @@ -147,9 +149,7 @@ def __init__( self.handler.set_transform(_param_update_formatter) -def _param_update_formatter( - record: logging.LogRecord, raw_msg: str -) -> Optional[str]: +def _param_update_formatter(record: logging.LogRecord, raw_msg: str) -> Optional[str]: """ A formater that makes parameter updates more prominent in the gui log window. """ diff --git a/src/instrumentserver/server/application.py b/src/instrumentserver/server/application.py index 2928048..ad7f098 100644 --- a/src/instrumentserver/server/application.py +++ b/src/instrumentserver/server/application.py @@ -72,7 +72,9 @@ def addInstrument(self, bp: InstrumentModuleBluePrint) -> None: def removeObject(self, name: str) -> None: items = self.findItems( - name, QtCore.Qt.MatchFlag.MatchExactly | QtCore.Qt.MatchFlag.MatchRecursive, 0 # type: ignore[arg-type] + name, + QtCore.Qt.MatchFlag.MatchExactly | QtCore.Qt.MatchFlag.MatchRecursive, + 0, # type: ignore[arg-type] ) if len(items) > 0: item = items[0] @@ -179,7 +181,10 @@ def __init__( insName: Optional[str] = None, kwargsStr: Optional[str] = None, parent: Optional[QtWidgets.QWidget] = None, - flags: Any = (QtCore.Qt.WindowType.CustomizeWindowHint | QtCore.Qt.WindowType.WindowCloseButtonHint), + flags: Any = ( + QtCore.Qt.WindowType.CustomizeWindowHint + | QtCore.Qt.WindowType.WindowCloseButtonHint + ), ) -> None: super().__init__(parent, flags) @@ -354,7 +359,9 @@ def addInstrumentToTree( """ insType = fullInsType.split(".")[-1] items = self.findItems( - insType, QtCore.Qt.MatchFlag.MatchExactly | QtCore.Qt.MatchFlag.MatchExactly, 0 # type: ignore[arg-type] + insType, + QtCore.Qt.MatchFlag.MatchExactly | QtCore.Qt.MatchFlag.MatchExactly, + 0, # type: ignore[arg-type] ) # Only add the instrument to the tree if there are no other instruments of the same type already @@ -380,7 +387,10 @@ def addInstrumentToTree( lineEdit.returnPressed.connect(lambda: createButton.clicked.emit()) lineEdit.setText(insName) item = PossibleInstrumentDisplayItem( - lst, fullInsType=fullInsType, configName=configName, lineEdit=lineEdit # type: ignore[arg-type] + lst, + fullInsType=fullInsType, + configName=configName, + lineEdit=lineEdit, # type: ignore[arg-type] ) parent.addChild(item) @@ -400,7 +410,9 @@ def onBasedInstrumentAction(self) -> None: if item.lineEdit is not None: # type: ignore[attr-defined] insName = item.lineEdit.text() # type: ignore[attr-defined] self.basedInstrumentRequested.emit( - item.configName, item.fullInsType, insName # type: ignore[attr-defined] + item.configName, + item.fullInsType, + insName, # type: ignore[attr-defined] ) @QtCore.Slot() @@ -445,9 +457,7 @@ class InstrumentsCreator(QtWidgets.QWidget): #: Arguments -- The str message of the error/reason as to why it could not create the instrument newInstrumentFailed = QtCore.Signal(object) - def __init__( - self, cli: Client, guiConfig: dict, *args: Any, **kwargs: Any - ) -> None: + def __init__(self, cli: Client, guiConfig: dict, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.guiConfig = guiConfig @@ -846,9 +856,7 @@ def displayComponentInfo(self, name: Union[str, None]) -> None: self.stationObjInfo.setObject(bp) # type: ignore[arg-type] @QtCore.Slot(QtWidgets.QTreeWidgetItem, int) - def addInstrumentTab( - self, item: QtWidgets.QTreeWidgetItem, index: int - ) -> None: + def addInstrumentTab(self, item: QtWidgets.QTreeWidgetItem, index: int) -> None: """ Gets called when the user double clicks and item of the instrument list. Adds a new generic instrument GUI window to the tab bar. @@ -888,9 +896,7 @@ def onTabDeleted(self, name: str) -> None: del self.instrumentTabsOpen[name] @QtCore.Slot(str, object, object, object) - def onFuncCalled( - self, n: str, args: Any, kw: Any, ret: Any - ) -> None: + def onFuncCalled(self, n: str, args: Any, kw: Any, ret: Any) -> None: if n == "close_and_remove_instrument": for ins in args: self.removeInstrumentFromGui(ins) @@ -960,9 +966,7 @@ def displayComponentInfo(self, name: Union[str, None]) -> None: self.stationObjInfo.setObject(self.client.getBluePrint(name)) @QtCore.Slot(QtWidgets.QTreeWidgetItem, int) - def addInstrumentTab( - self, item: QtWidgets.QTreeWidgetItem, index: int - ) -> None: + def addInstrumentTab(self, item: QtWidgets.QTreeWidgetItem, index: int) -> None: name = item.text(0) if name not in self.instrumentTabsOpen: ins = self.client.find_or_create_instrument(name) diff --git a/src/instrumentserver/server/core.py b/src/instrumentserver/server/core.py index 7ed9233..784b8e3 100644 --- a/src/instrumentserver/server/core.py +++ b/src/instrumentserver/server/core.py @@ -579,7 +579,9 @@ def _broadcastParameterChange(self, blueprint: ParameterBroadcastBluePrint) -> N sendBroadcast(self.broadcastSocket, blueprint.name.split(".")[0], blueprint) # type: ignore[arg-type] if self.externalBroadcastAddr is not None: sendBroadcast( - self.externalBroadcastSocket, blueprint.name.split(".")[0], blueprint # type: ignore[arg-type] + self.externalBroadcastSocket, + blueprint.name.split(".")[0], + blueprint, # type: ignore[arg-type] ) logger.info( f"Parameter {blueprint.name} has broadcast an update of type: {blueprint.action}," diff --git a/test/notebooks/Autoupdate.ipynb b/test/notebooks/Autoupdate.ipynb index 6d488dd..a82f194 100644 --- a/test/notebooks/Autoupdate.ipynb +++ b/test/notebooks/Autoupdate.ipynb @@ -6,10 +6,7 @@ "id": "a537f51e-cd6f-406f-9bb7-59f80133e622", "metadata": {}, "outputs": [], - "source": [ - "\n", - "\n" - ] + "source": [] }, { "cell_type": "code", diff --git a/test/notebooks/Prototype the ParamManager.ipynb b/test/notebooks/Prototype the ParamManager.ipynb index ef89282..ff1b4c3 100644 --- a/test/notebooks/Prototype the ParamManager.ipynb +++ b/test/notebooks/Prototype the ParamManager.ipynb @@ -30,7 +30,6 @@ }, "outputs": [], "source": [ - "\n", "from qcodes import Instrument, Station\n", "from qcodes.utils import validators\n", "\n", diff --git a/test/pytest/test_client_station.py b/test/pytest/test_client_station.py index db5fb8f..495ceee 100644 --- a/test/pytest/test_client_station.py +++ b/test/pytest/test_client_station.py @@ -1,6 +1,5 @@ """Tests for ClientStation and ClientStationGui.""" - import pytest from instrumentserver.client.proxy import ClientStation From 3cddf38d0d77c53efb1ec67b4a06f1464028bc49 Mon Sep 17 00:00:00 2001 From: marcosf2 Date: Thu, 30 Apr 2026 17:00:26 -0500 Subject: [PATCH 12/12] Refactor for type casting clarity using `cast`, update Python version to 3.13 in `ci.yml`. --- .github/workflows/ci.yml | 12 ++--- src/instrumentserver/client/application.py | 22 ++++++--- src/instrumentserver/gui/base_instrument.py | 21 ++++++--- src/instrumentserver/gui/instruments.py | 52 +++++++++++++++------ src/instrumentserver/gui/misc.py | 10 ++-- src/instrumentserver/server/application.py | 29 +++++++----- src/instrumentserver/server/core.py | 6 ++- 7 files changed, 101 insertions(+), 51 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 40d4201..c1d092e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: - uses: astral-sh/setup-uv@v7 with: enable-cache: true - python-version: "3.12" + python-version: "3.13" - run: uv lock --locked lint: @@ -26,7 +26,7 @@ jobs: - uses: astral-sh/setup-uv@v7 with: enable-cache: true - python-version: "3.12" + python-version: "3.13" - run: uv sync --locked --group dev - run: uv run ruff check @@ -38,7 +38,7 @@ jobs: - uses: astral-sh/setup-uv@v7 with: enable-cache: true - python-version: "3.12" + python-version: "3.13" - run: uv sync --locked --group dev - run: uv run ruff format --check @@ -50,7 +50,7 @@ jobs: - uses: astral-sh/setup-uv@v7 with: enable-cache: true - python-version: "3.12" + python-version: "3.13" - run: uv sync --locked --group dev - run: uv run mypy @@ -64,7 +64,7 @@ jobs: - uses: astral-sh/setup-uv@v7 with: enable-cache: true - python-version: "3.12" + python-version: "3.13" - name: Install system deps for PyQt5 run: | sudo apt-get update @@ -84,5 +84,5 @@ jobs: - uses: astral-sh/setup-uv@v7 with: enable-cache: true - python-version: "3.12" + python-version: "3.13" - run: uv build diff --git a/src/instrumentserver/client/application.py b/src/instrumentserver/client/application.py index be4248e..9c04582 100644 --- a/src/instrumentserver/client/application.py +++ b/src/instrumentserver/client/application.py @@ -2,7 +2,7 @@ import logging import sys from pathlib import Path -from typing import Dict, Optional, Union +from typing import Dict, Optional, Union, cast from qtpy.QtGui import QGuiApplication from qtpy.QtWidgets import QFileDialog, QWidget @@ -39,8 +39,12 @@ def __init__( form_layout.setContentsMargins(0, 0, 0, 0) form_layout.setSpacing(8) form_layout.setLabelAlignment( - QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter - ) # type: ignore[arg-type] + cast( + "QtCore.Qt.Alignment", + QtCore.Qt.AlignmentFlag.AlignRight + | QtCore.Qt.AlignmentFlag.AlignVCenter, + ) + ) form_layout.setFieldGrowthPolicy( QtWidgets.QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow ) @@ -342,11 +346,15 @@ def loadParams(self) -> None: # Refresh all tabs for i in range(self.tabs.count()): widget = self.tabs.widget(i) - if hasattr(widget, "parametersList") and hasattr( - widget.parametersList, - "model", # type: ignore[union-attr] + if ( + widget is not None + and hasattr(widget, "parametersList") + and hasattr( + widget.parametersList, + "model", + ) ): - widget.parametersList.model.refreshAll() # type: ignore[union-attr] + widget.parametersList.model.refreshAll() except Exception as e: QtWidgets.QMessageBox.critical(self, "Load Error", str(e)) diff --git a/src/instrumentserver/gui/base_instrument.py b/src/instrumentserver/gui/base_instrument.py index 8e5996b..5e76af5 100644 --- a/src/instrumentserver/gui/base_instrument.py +++ b/src/instrumentserver/gui/base_instrument.py @@ -104,7 +104,7 @@ import fnmatch from pprint import pprint -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, cast from instrumentserver import QtCore, QtGui, QtWidgets @@ -313,8 +313,12 @@ def addItem(self, fullName: str, **kwargs: Any) -> "ItemBase": items = self.findItems( smName, - QtCore.Qt.MatchFlag.MatchExactly | QtCore.Qt.MatchFlag.MatchRecursive, - 0, # type: ignore[arg-type] + cast( + "QtCore.Qt.MatchFlags", + QtCore.Qt.MatchFlag.MatchExactly + | QtCore.Qt.MatchFlag.MatchRecursive, + ), + 0, ) if len(items) == 0: @@ -347,8 +351,11 @@ def addItem(self, fullName: str, **kwargs: Any) -> "ItemBase": def removeItem(self, fullName: str) -> None: items = self.findItems( fullName, - QtCore.Qt.MatchFlag.MatchExactly | QtCore.Qt.MatchFlag.MatchRecursive, - 0, # type: ignore[arg-type] + cast( + "QtCore.Qt.MatchFlags", + QtCore.Qt.MatchFlag.MatchExactly | QtCore.Qt.MatchFlag.MatchRecursive, + ), + 0, ) if len(items) > 0: @@ -631,8 +638,8 @@ def restoreCollapsedDict(self) -> None: for x in self.delegateColumns # type: ignore[union-attr] ] proxyDelegateIndexes = [ - self.model().mapFromSource(index) - for index in delegateIndexes # type: ignore[union-attr] + self.model().mapFromSource(index) # type: ignore[union-attr] + for index in delegateIndexes ] for delegateIndex in proxyDelegateIndexes: self.openPersistentEditor(delegateIndex) diff --git a/src/instrumentserver/gui/instruments.py b/src/instrumentserver/gui/instruments.py index bca6567..1a41ae0 100644 --- a/src/instrumentserver/gui/instruments.py +++ b/src/instrumentserver/gui/instruments.py @@ -1,6 +1,6 @@ import inspect import logging -from typing import Any, Callable, Dict, Optional, Union +from typing import Any, Callable, Dict, Optional, Union, cast from qcodes import Instrument @@ -54,24 +54,36 @@ def __init__( self.nameEdit = QtWidgets.QLineEdit(self) lbl = QtWidgets.QLabel("Name:") lbl.setAlignment( - QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter - ) # type: ignore[arg-type] + cast( + "QtCore.Qt.Alignment", + QtCore.Qt.AlignmentFlag.AlignRight + | QtCore.Qt.AlignmentFlag.AlignVCenter, + ) + ) layout.addWidget(lbl, 0, 0) layout.addWidget(self.nameEdit, 0, 1) self.valueEdit = QtWidgets.QLineEdit(self) lbl = QtWidgets.QLabel("Value:") lbl.setAlignment( - QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter - ) # type: ignore[arg-type] + cast( + "QtCore.Qt.Alignment", + QtCore.Qt.AlignmentFlag.AlignRight + | QtCore.Qt.AlignmentFlag.AlignVCenter, + ) + ) layout.addWidget(lbl, 0, 2) layout.addWidget(self.valueEdit, 0, 3) self.unitEdit = QtWidgets.QLineEdit(self) lbl = QtWidgets.QLabel("Unit:") lbl.setAlignment( - QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter - ) # type: ignore[arg-type] + cast( + "QtCore.Qt.Alignment", + QtCore.Qt.AlignmentFlag.AlignRight + | QtCore.Qt.AlignmentFlag.AlignVCenter, + ) + ) layout.addWidget(lbl, 0, 4) layout.addWidget(self.unitEdit, 0, 5) @@ -87,9 +99,12 @@ def __init__( ) lbl = QtWidgets.QLabel("Type:") lbl.setAlignment( - QtCore.Qt.AlignmentFlag.AlignRight - | QtCore.Qt.AlignmentFlag.AlignVCenter - ) # type: ignore[arg-type] + cast( + "QtCore.Qt.Alignment", + QtCore.Qt.AlignmentFlag.AlignRight + | QtCore.Qt.AlignmentFlag.AlignVCenter, + ) + ) layout.addWidget(lbl, 1, 0) layout.addWidget(self.typeSelect, 1, 1) @@ -104,9 +119,12 @@ def __init__( "See qcodes.utils.validators for details." ) lbl.setAlignment( - QtCore.Qt.AlignmentFlag.AlignRight - | QtCore.Qt.AlignmentFlag.AlignVCenter - ) # type: ignore[arg-type] + cast( + "QtCore.Qt.Alignment", + QtCore.Qt.AlignmentFlag.AlignRight + | QtCore.Qt.AlignmentFlag.AlignVCenter, + ) + ) layout.addWidget(lbl, 1, 2) layout.addWidget(self.valsArgsEdit, 1, 3) @@ -348,8 +366,12 @@ def updateParameter(self, bp: ParameterBroadcastBluePrint) -> None: elif bp.action == "parameter-update" or bp.action == "parameter-call": item = self.findItems( fullName, - QtCore.Qt.MatchFlag.MatchExactly | QtCore.Qt.MatchFlag.MatchRecursive, - 0, # type: ignore[arg-type] + cast( + "QtCore.Qt.MatchFlags", + QtCore.Qt.MatchFlag.MatchExactly + | QtCore.Qt.MatchFlag.MatchRecursive, + ), + 0, ) if len(item) == 0: if fullName not in self.itemsHide: # type: ignore[operator] diff --git a/src/instrumentserver/gui/misc.py b/src/instrumentserver/gui/misc.py index 1b5ceeb..9e7f6fb 100644 --- a/src/instrumentserver/gui/misc.py +++ b/src/instrumentserver/gui/misc.py @@ -1,4 +1,4 @@ -from typing import Any, Optional, Tuple +from typing import Any, Optional, Tuple, cast from .. import QtCore, QtGui, QtWidgets @@ -12,8 +12,12 @@ def __init__( super().__init__(parent) self.setAlignment( - QtCore.Qt.AlignmentFlag.AlignVCenter | QtCore.Qt.AlignmentFlag.AlignHCenter - ) # type: ignore[arg-type] + cast( + "QtCore.Qt.Alignment", + QtCore.Qt.AlignmentFlag.AlignVCenter + | QtCore.Qt.AlignmentFlag.AlignHCenter, + ) + ) self._pixmapSize = pixmapSize pix = QtGui.QIcon(":/icons/no-alert.svg").pixmap(*pixmapSize) self.setPixmap(pix) diff --git a/src/instrumentserver/server/application.py b/src/instrumentserver/server/application.py index ad7f098..83fcc0a 100644 --- a/src/instrumentserver/server/application.py +++ b/src/instrumentserver/server/application.py @@ -4,7 +4,7 @@ import os import sys import time -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Tuple, Union, cast from instrumentserver.client import QtClient from instrumentserver.log import LogLevels, LogWidget, log @@ -73,8 +73,11 @@ def addInstrument(self, bp: InstrumentModuleBluePrint) -> None: def removeObject(self, name: str) -> None: items = self.findItems( name, - QtCore.Qt.MatchFlag.MatchExactly | QtCore.Qt.MatchFlag.MatchRecursive, - 0, # type: ignore[arg-type] + cast( + "QtCore.Qt.MatchFlags", + QtCore.Qt.MatchFlag.MatchExactly | QtCore.Qt.MatchFlag.MatchRecursive, + ), + 0, ) if len(items) > 0: item = items[0] @@ -360,8 +363,11 @@ def addInstrumentToTree( insType = fullInsType.split(".")[-1] items = self.findItems( insType, - QtCore.Qt.MatchFlag.MatchExactly | QtCore.Qt.MatchFlag.MatchExactly, - 0, # type: ignore[arg-type] + cast( + "QtCore.Qt.MatchFlags", + QtCore.Qt.MatchFlag.MatchExactly | QtCore.Qt.MatchFlag.MatchExactly, + ), + 0, ) # Only add the instrument to the tree if there are no other instruments of the same type already @@ -382,7 +388,7 @@ def addInstrumentToTree( createButton = QtWidgets.QPushButton("Create") - lst = [configName, insName, "create"] + lst: list[str] = [configName or "", insName, "create"] lineEdit = QtWidgets.QLineEdit() lineEdit.returnPressed.connect(lambda: createButton.clicked.emit()) lineEdit.setText(insName) @@ -390,7 +396,7 @@ def addInstrumentToTree( lst, fullInsType=fullInsType, configName=configName, - lineEdit=lineEdit, # type: ignore[arg-type] + lineEdit=lineEdit, ) parent.addChild(item) @@ -405,14 +411,15 @@ def addInstrumentToTree( def onBasedInstrumentAction(self) -> None: items = self.selectedItems() - for item in items: + for raw_item in items: + item = cast(PossibleInstrumentDisplayItem, raw_item) insName = None - if item.lineEdit is not None: # type: ignore[attr-defined] - insName = item.lineEdit.text() # type: ignore[attr-defined] + if item.lineEdit is not None: + insName = item.lineEdit.text() self.basedInstrumentRequested.emit( item.configName, item.fullInsType, - insName, # type: ignore[attr-defined] + insName, ) @QtCore.Slot() diff --git a/src/instrumentserver/server/core.py b/src/instrumentserver/server/core.py index 784b8e3..52342bb 100644 --- a/src/instrumentserver/server/core.py +++ b/src/instrumentserver/server/core.py @@ -576,12 +576,14 @@ def _broadcastParameterChange(self, blueprint: ParameterBroadcastBluePrint) -> N :param blueprint: The parameter broadcast blueprint that is being broadcast """ - sendBroadcast(self.broadcastSocket, blueprint.name.split(".")[0], blueprint) # type: ignore[arg-type] + assert self.broadcastSocket is not None + sendBroadcast(self.broadcastSocket, blueprint.name.split(".")[0], blueprint) if self.externalBroadcastAddr is not None: + assert self.externalBroadcastSocket is not None sendBroadcast( self.externalBroadcastSocket, blueprint.name.split(".")[0], - blueprint, # type: ignore[arg-type] + blueprint, ) logger.info( f"Parameter {blueprint.name} has broadcast an update of type: {blueprint.action},"