diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c1d092e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,88 @@ +name: Code quality + +on: + pull_request: + 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.13" + - 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.13" + - run: uv sync --locked --group dev + - run: uv run ruff check + + format: + runs-on: ubuntu-latest + needs: lockfile + steps: + - uses: actions/checkout@v5 + - uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + python-version: "3.13" + - 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.13" + - run: uv sync --locked --group dev + - run: uv run mypy + + test: + runs-on: ubuntu-latest + needs: lockfile + env: + QT_QPA_PLATFORM: offscreen + steps: + - uses: actions/checkout@v5 + - uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + python-version: "3.13" + - name: Install system deps for PyQt5 + run: | + sudo apt-get update + sudo apt-get install -y \ + libgl1 \ + libegl1 \ + libxkbcommon0 \ + libdbus-1-3 + - run: uv sync --locked --group dev + - run: 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.13" + - run: uv build 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/instrumentserver/client/__init__.py b/instrumentserver/client/__init__.py deleted file mode 100644 index 6213417..0000000 --- a/instrumentserver/client/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .core import sendRequest -from .proxy import ProxyInstrument, Client, QtClient, SubClient, ClientStation - diff --git a/instrumentserver/gui/__init__.py b/instrumentserver/gui/__init__.py deleted file mode 100644 index c9406ef..0000000 --- a/instrumentserver/gui/__init__.py +++ /dev/null @@ -1,48 +0,0 @@ -from .. import QtCore, QtWidgets, resource - - -def getStyleSheet(): - f = QtCore.QFile(":/style.css") - if f.open(QtCore.QIODevice.ReadOnly | QtCore.QIODevice.Text): - style = f.readAll() - f.close() - return str(style, 'utf-8') - - -def widgetDialog(w: QtWidgets.QWidget): - 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. - - css = getStyleSheet() - w.setStyleSheet(css) - - lay = QtWidgets.QVBoxLayout(dg) - lay.addWidget(w) - lay.setContentsMargins(0, 0, 0, 0) - dg.setLayout(lay) - - dg.show() - return dg - - -def widgetMainWindow(w: QtWidgets.QWidget, name: str = 'instrumentserver'): - mw = QtWidgets.QMainWindow() - mw.setWindowTitle(name) - mw.setCentralWidget(w) - - css = getStyleSheet() - w.setStyleSheet(css) - - mw.show() - return mw - - -def keepSmallHorizontally(w: QtWidgets.QWidget): - w.setSizePolicy( - QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, - QtWidgets.QSizePolicy.Minimum) - ) - diff --git a/instrumentserver/testing/dummy_instruments/generic.py b/instrumentserver/testing/dummy_instruments/generic.py deleted file mode 100644 index 1a64398..0000000 --- a/instrumentserver/testing/dummy_instruments/generic.py +++ /dev/null @@ -1,226 +0,0 @@ -# mypy: ignore-errors -# No need to mypy check dummy testing instruments. - -from typing import List - -from qcodes import Instrument -from qcodes.utils import validators -from qcodes.math_utils.field_vector import FieldVector -import numpy as np -import time - - -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.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:') - 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): - 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('Chan{}'.format(chan_name)) - self.add_submodule(chan_name, channel) - - self.functions['test_func'] = self.test_func - self.functions['dummy_function'] = self.dummy_function - - def test_func(self, a, b, *args, c: List[int] = [10, 11], **kwargs): - """ - This is a test function, of course you knew this from the tittle but It's nice to have documentation, isn't it? - - :param a: Very nice parameter. - :param b: Even nicer parameter - :param c: This one sucks though. - """ - 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}') - 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): - super().__init__(name, *args, **kwargs) - - self.random = np.random.randint(10000) - self._param1 = 1 - 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)) - - def _get_param1(self): - # for testing potentially redundant/duplicate get calls - 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 - - def get_random_timeout(self, wait_time=10): - time.sleep(wait_time) - 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('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('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) - - def generate_data(self, name: str): - - if name == 'param0': - self.parameters[name].set(np.random.randint(1, 10)) - if name == 'param1': - self.parameters[name].set(np.random.randint(10, 20)) - if name == 'param2': - self.parameters[name].set(np.random.randint(20, 30)) - if name == 'param3': - self.parameters[name].set(np.random.randint(30, 40)) - if name == 'param4': - self.parameters[name].set(np.random.randint(40, 50)) - - def get(self, param_name): - self.generate_data(param_name) - return self.parameters[param_name].get() - - -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) - - self.field_vector = FieldVector(x=1, y=1, z=1) - self.complex_value = 1 + 1j - 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, - ) - - def get_starting_parameter(self): - return self.starting_parameter - - def set_starting_parameter(self, new_value): - pass - - def get_field(self): - return self.field_vector - - def set_field(self, field_vector: FieldVector): - self.field_vector = field_vector - - def get_complex(self): - return self.complex_value - - def set_complex(self, value: complex): - self.complex_value = value - - def get_complex_list(self): - return self.complex_lst - - def set_complex_list(self, value): - self.complex_lst = value - - def generic_function(self): - print(f'this generic function has been called') - return 3 diff --git a/instrumentserver/testing/dummy_instruments/rf.py b/instrumentserver/testing/dummy_instruments/rf.py deleted file mode 100644 index 4fc9d2a..0000000 --- a/instrumentserver/testing/dummy_instruments/rf.py +++ /dev/null @@ -1,162 +0,0 @@ -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 - - - -class ResonatorResponse(Instrument): - """A dummy instrument that generates the response of a resonator measured in - reflection. - - Behavior is essentially that of a VNA, with the resonator and system - properties added as parameters. - """ - - def __init__(self, name, f0=5e9, df=1e6, **kw): - super().__init__(name, **kw) - - self._frq_mod = 0.0 - 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) - - # 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) - - # 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, ) - - def modulate_frequency(self, delta: float = 0, multiply=False) -> None: - """Add an offset to the resonance frequency. - - If `multiply` is ``True``, the change in frequency is the product of `delta` - and the set frequency. If ``False``, then `delta` is added. - """ - self._frq_mod = delta - self._frq_mod_multiply = multiply - - # private utility methods - def _frequency_vals(self): - return np.linspace(self.start_frequency(), self.stop_frequency(), self.npoints()) - - def _get_data(self): - f0 = self.resonator_frequency() - if self._frq_mod_multiply: - f0 *= self._frq_mod - else: - f0 += self._frq_mod - - fvals = self._frequency_vals() - data = self._resonator_reflection_signal( - fvals, - f0, - self.resonator_linewidth(), - self.power() - self.input_attenuation(), - self.bandwidth(), - self.noise_temperature()) - - return data - - def _resonator_reflection_signal(self, fvals, f0, df, P_in, BW, T_N): - """Compute a realistic resonator reflection signal of a one-port - resonator, including random noise. - - :param fvals: probe frequencies [Hz] - :param f0: resonance frequency [Hz] - :param df: line width [Hz] - :param P_in: incident power [dBm] - :param BW: detection bandwidth [Hz] - :param T_N: noise temperature [K] - - :returns: dummy data, same shape as `fvals`. - """ - 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_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 - - -class Generator(Instrument): - """A simple dummy that mocks an RF generator.""" - - 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('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) - - -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): - 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) - - def _set_flux(self, flux): - mod = 1./(1. + self.inductive_participation_ratio() / np.abs(np.cos(np.pi*flux))) - self._resonator.modulate_frequency(mod, True) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c43b5f6 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,114 @@ +[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", + "numpy", + "pandas", + "jsonschema", + "ruamel.yaml", + "PyYAML", +] + +[project.optional-dependencies] +monitoring = ["influxdb-client"] + +[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"] + +[tool.ruff] +exclude = ["docs"] + +[tool.ruff.lint] +extend-select = ["I"] + +[tool.ruff.lint.per-file-ignores] +"*.ipynb" = ["E402"] + +[tool.mypy] +files = ["src"] +exclude = ["src/instrumentserver/resource\\.py$"] +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.*", + "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/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 57% rename from instrumentserver/__init__.py rename to src/instrumentserver/__init__.py index 482657e..c8b9129 100644 --- a/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: @@ -20,15 +20,15 @@ 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 with open(PARAMS_SCHEMA_PATH) as f: paramDictSchema = json.load(f) -from .log import setupLogging, logger +from .client import Client # noqa: E402 +from .log import logger as logger # noqa: E402 +from .log import setupLogging as setupLogging # noqa: E402 -from .client import Client -InstrumentClient = Client \ No newline at end of file +InstrumentClient = Client diff --git a/instrumentserver/apps.py b/src/instrumentserver/apps.py similarity index 56% rename from instrumentserver/apps.py rename to src/instrumentserver/apps.py index 8610a73..0d881d1 100644 --- a/instrumentserver/apps.py +++ b/src/instrumentserver/apps.py @@ -1,31 +1,29 @@ -import os import argparse import logging +import os import signal from pathlib import Path +from typing import Any, Optional -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') +setupLogging(addStreamHandler=True, logFile=os.path.abspath("instrumentserver.log")) +logger = logging.getLogger("instrumentserver") logger.setLevel(logging.INFO) -def server(**kwargs): +def server(**kwargs: Any) -> int: app = QtCore.QCoreApplication([]) # this allows us to kill the server by KeyboardInterrupt @@ -36,31 +34,45 @@ def server(**kwargs): return app.exec_() -def serverWithGui(**kwargs): +def serverWithGui(**kwargs: Any) -> int: app = QtWidgets.QApplication([]) - window = startServerGuiApplication(**kwargs) + startServerGuiApplication(**kwargs) return app.exec_() 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 +80,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 +112,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 +128,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,18 +153,22 @@ 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([]) - 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() app.exec_() - - diff --git a/instrumentserver/base.py b/src/instrumentserver/base.py similarity index 65% rename from instrumentserver/base.py rename to src/instrumentserver/base.py index 5b489b0..b5623ef 100644 --- a/instrumentserver/base.py +++ b/src/instrumentserver/base.py @@ -1,61 +1,64 @@ -import zmq import json import logging +from typing import Any, Tuple, Union + +import zmq -from .blueprints import to_dict, deserialize_obj +from .blueprints import deserialize_obj, to_dict 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) else: - return socket.send(payload.encode('utf-8')) + 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 + 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 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]) + 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 - 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}") 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. @@ -65,10 +68,10 @@ 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): +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/instrumentserver/blueprints.py b/src/instrumentserver/blueprints.py similarity index 75% rename from instrumentserver/blueprints.py rename to src/instrumentserver/blueprints.py index e8524dd..94fb288 100644 --- a/instrumentserver/blueprints.py +++ b/src/instrumentserver/blueprints.py @@ -53,29 +53,28 @@ 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) + Instrument, + InstrumentChannel, + Parameter, + ParameterWithSetpoints, +) from qcodes.instrument.base import InstrumentBase -from qcodes.utils.validators import Validator from .helpers import objectClassPath, typeClassPath 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 +82,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) @@ -100,8 +100,8 @@ def __repr__(self) -> str: def __str__(self) -> str: return f"{self.name}: {self.parameter_class}" - def tostr(self, indent=0): - i = indent * ' ' + def tostr(self, indent: int = 0) -> str: + i = indent * " " ret = f"""{self.name}: {self.parameter_class} {i}- unit: {self.unit} {i}- path: {self.path} @@ -112,20 +112,23 @@ def tostr(self, indent=0): """ return ret - def toJson(self): + def toJson(self) -> Dict[str, Any]: 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 +136,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,21 +150,22 @@ 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): + 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): - i = indent * ' ' + def tostr(self, indent: int = 0) -> str: + i = indent * " " ret = f"""{self.name}{str(self.call_signature_str)} {i}- path: {self.path} """ @@ -169,14 +173,16 @@ 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(): param_dict[name] = str(param.kind) return call_signature_str, param_dict - def toJson(self): + def toJson(self) -> Dict[str, Any]: return bluePrintToDict(self) @@ -184,7 +190,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 +202,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 +267,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) @@ -263,40 +275,42 @@ def __repr__(self) -> str: def __str__(self) -> str: return f"{self.name}: {self.instrument_module_class}" - def tostr(self, indent=0): - i = indent * ' ' + 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) -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 +334,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,41 +356,45 @@ 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}}""" + ret = ret + f'\n "unit":"{self.unit}"' + ret = ret + """\n}""" return ret - def __repr__(self): + def __repr__(self) -> str: return str(self) - def pprint(self, indent=0): - - i = indent * ' ' + def pprint(self, indent: int = 0) -> str: + i = indent * " " ret = f"""name: {self.name} {i}- action: {self.action} {i}- value: {self.value} {i}- unit: {self.unit} -{i}- bp_type: {self.bp_type} """ return ret - def toJson(self): + def toJson(self) -> Dict[str, Any]: return bluePrintToDict(self) -BluePrintType = Union[ParameterBluePrint, MethodBluePrint, InstrumentModuleBluePrint, ParameterBroadcastBluePrint] +BluePrintType = Union[ + ParameterBluePrint, + MethodBluePrint, + InstrumentModuleBluePrint, + ParameterBroadcastBluePrint, +] def _dictToJson(_dict: dict, json_type: bool = True) -> dict: @@ -394,7 +412,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. @@ -408,7 +426,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 +442,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 +472,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 +480,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): + 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) + ret["args"] = iterable_to_serialized_dict(self.args) + ret["kwargs"] = dict_to_serialized_dict(self.kwargs) return ret @@ -483,12 +503,12 @@ class CallSpec: #: kw args to pass. kwargs: Optional[Dict[str, Any]] = None - _class_type: str = '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) + ret["args"] = iterable_to_serialized_dict(self.args) + ret["kwargs"] = dict_to_serialized_dict(self.kwargs) return ret @@ -498,7 +518,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 +528,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): + 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) + ret["args"] = iterable_to_serialized_dict(self.args) + ret["kwargs"] = dict_to_serialized_dict(self.kwargs) return ret @@ -578,48 +598,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): + 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.') + 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)} + 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 + 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 +655,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 +663,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: @@ -661,36 +685,40 @@ def __init__(self, message: Optional[Any] = None, message = message.replace("none", "null") 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}') + except json.JSONDecodeError: + 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 = {} + 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'): - 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 +730,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,27 +747,29 @@ 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 -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. @@ -766,24 +796,30 @@ 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=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: 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 -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. """ @@ -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=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) @@ -814,7 +854,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. @@ -825,12 +865,12 @@ 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. """ 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: @@ -851,20 +891,20 @@ 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 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') + except json.JSONDecodeError: + 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 new file mode 100644 index 0000000..7253c60 --- /dev/null +++ b/src/instrumentserver/client/__init__.py @@ -0,0 +1,6 @@ +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/instrumentserver/client/application.py b/src/instrumentserver/client/application.py similarity index 70% rename from instrumentserver/client/application.py rename to src/instrumentserver/client/application.py index cd6b5cc..9c04582 100644 --- a/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 Dict, Optional, Union, cast -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__" @@ -28,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 @@ -37,8 +38,16 @@ 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( + cast( + "QtCore.Qt.Alignment", + QtCore.Qt.AlignmentFlag.AlignRight + | QtCore.Qt.AlignmentFlag.AlignVCenter, + ) + ) + form_layout.setFieldGrowthPolicy( + QtWidgets.QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow + ) # Non-editable self.host = QtWidgets.QLineEdit(self.client_station._host) @@ -58,7 +67,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) @@ -81,16 +92,15 @@ 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) # 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,23 +116,32 @@ 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.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() @@ -131,8 +150,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) @@ -140,36 +159,35 @@ 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]) - 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() + 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): - if message.action == 'parameter-update': + 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. @@ -181,25 +199,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) @@ -211,7 +236,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: @@ -219,23 +244,27 @@ 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.parametersList.model.refreshAll() + if hasattr(widget, "parametersList") and ( + widget.objectName() in self.instrumentTabsOpen # type: ignore[union-attr] + ): + 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 @@ -248,7 +277,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 +289,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 +301,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) @@ -281,7 +310,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 (*)" ) @@ -289,10 +318,12 @@ 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(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) @@ -301,10 +332,12 @@ 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(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 +346,28 @@ 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 ( + widget is not None + and 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) -> None: # , 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,8 +375,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() @@ -343,8 +383,15 @@ 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: + 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/instrumentserver/client/core.py b/src/instrumentserver/client/core.py similarity index 60% rename from instrumentserver/client/core.py rename to src/instrumentserver/client/core.py index 27f65fb..e55a980 100644 --- a/instrumentserver/client/core.py +++ b/src/instrumentserver/client/core.py @@ -1,17 +1,18 @@ import logging +import uuid import warnings +from types import TracebackType +from typing import Any, Optional, Type + 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__) - 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,10 +26,18 @@ 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: str = "localhost", + port: int = DEFAULT_PORT, + connect: bool = True, + timeout: float = 20, + raise_exceptions: bool = True, + ) -> None: self.connected = False - self.context = None - self.socket = None + self._closed = False + self.context: Optional[zmq.Context] = None + self.socket: Optional[zmq.Socket] = None self.host = host self.port = port self.addr = f"tcp://{host}:{port}" @@ -38,32 +47,55 @@ def __init__(self, host='localhost', port=DEFAULT_PORT, connect=True, timeout=20 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 + # 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) 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 - def ask(self, message): - if not self.connected: + 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) - logger.debug(f"Response received.") + 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: self._reset_connection() @@ -72,7 +104,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: @@ -80,16 +112,17 @@ def ask(self, message): return ret.message return ret - - def _reset_connection(self): + + def _reset_connection(self) -> None: try: if self.socket is not None: self.socket.close(linger=0) finally: self.connected = False - self.connect() - - def _handle_server_error(self, err): + if not self._closed: + self.connect() + + def _handle_server_error(self, err: Any) -> None: if isinstance(err, str): logger.error(err) if self.raise_exceptions: @@ -105,19 +138,25 @@ def _handle_server_error(self, err): if self.raise_exceptions: raise TypeError(msg) logger.error(msg) - - def disconnect(self): + + def disconnect(self) -> None: + self._closed = True 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 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/instrumentserver/client/proxy.py b/src/instrumentserver/client/proxy.py similarity index 66% rename from instrumentserver/client/proxy.py rename to src/instrumentserver/client/proxy.py index 513f53e..3c43d74 100644 --- a/instrumentserver/client/proxy.py +++ b/src/instrumentserver/client/proxy.py @@ -4,37 +4,38 @@ @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, Callable, 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__) @@ -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: Any, + 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: Any, + ) -> None: self.cli = cli self.host = host @@ -73,35 +77,31 @@ 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) 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): - req = ServerInstruction( - operation=Operation.get_blueprint, - requested_path=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( - target=self.remotePath + '.snapshot', args=args, kwargs=kwargs - ) + target=self.remotePath + ".snapshot", args=args, kwargs=kwargs + ), ) return self.askServer(req) @@ -119,57 +119,67 @@ 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: 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: Any, + ) -> None: + + 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) - - def initKwargsFromBluePrint(self, bp): - kwargs = {} + setpoints = [ + getattr(setpoints_instrument, setpoint) + for setpoint in self.bp.setpoints + ] + setattr(self, "setpoints", setpoints) + + def initKwargsFromBluePrint(self, bp: Any) -> Dict[str, Any]: + kwargs: Dict[str, Any] = {} 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): + def _remoteSet(self, value: Any) -> 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) - def _remoteGet(self): + def _remoteGet(self) -> Any: msg = ServerInstruction( operation=Operation.call, call_spec=CallSpec( target=self.remotePath, - ) + ), ) return self.askServer(msg) @@ -184,31 +194,44 @@ 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: Any, + cli: Optional["Client"] = None, + host: Optional[str] = "localhost", + port: Optional[int] = DEFAULT_PORT, + remotePath: Optional[str] = None, + bluePrint: Optional[InstrumentModuleBluePrint] = None, + **kwargs: Any, + ) -> None: + + 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. 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): - obj.cli.call(f'{obj.remotePath}.remove_parameter', name) + if mn == "remove_parameter": + + 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 + 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 @@ -219,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: @@ -227,18 +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. @@ -247,9 +269,11 @@ 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): + 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, @@ -259,25 +283,21 @@ 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: 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() - 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 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() @@ -294,8 +314,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(): @@ -306,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. """ @@ -317,11 +344,11 @@ 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) + call_spec=CallSpec(target=bp.path, args=a, kwargs=k), ) return self.askServer(msg) @@ -331,8 +358,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,27 +372,28 @@ 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} - _ret = exec(new_func_str, globs) + globs = {"wrap": wrap, "qcodes": qc, "collections": collections} + 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. """ 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) - self.add_submodule(sn, submodule) + s.name, cli=self.cli, host=self.host, port=self.port, bluePrint=s + ) + self.add_submodule(sn, submodule) # type: ignore[type-var] else: self.submodules[sn].update() @@ -373,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: @@ -384,16 +415,17 @@ 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) - self.add_submodule(sn, submodule) + s.name, cli=self.cli, host=self.host, port=self.port, bluePrint=s + ) + 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 @@ -414,24 +446,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: 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]: - """ 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,43 +495,42 @@ 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) + 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( target=target, args=args, kwargs=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: @@ -502,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: @@ -513,22 +561,29 @@ 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): + def get_snapshot( + self, instrument: str | None = None, *args: Any, **kwargs: Any + ) -> Any: 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: Any, + **kwargs: Any, + ) -> Any: msg = ServerInstruction( operation=Operation.get_param_dict, serialization_opts=ParameterSerializeSpec( @@ -536,11 +591,17 @@ 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: Any, + **kwargs: Any, + ) -> None: filePath = os.path.abspath(filePath) folder, file = os.path.split(filePath) @@ -550,7 +611,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( # type: ignore[misc] + instrument=instrument_name, *args, **kwargs + ) params.update(inst_params) # Convert to nested format before saving, @@ -558,20 +621,22 @@ 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]): + 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: + 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 +647,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 +659,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 +678,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. @@ -623,11 +700,11 @@ def __init__(self, instruments: Optional[List[str]] = None, sub_host: str = 'loc 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. @@ -643,7 +720,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) @@ -674,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. """ @@ -689,26 +766,45 @@ 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): + def __init__( + self, + 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: 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 + # 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, - config_path: str = None, - param_path: str = None): + def __init__( + self, + 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. @@ -762,6 +858,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 @@ -771,56 +868,77 @@ def __init__(self, host='localhost', port=DEFAULT_PORT, connect=True, timeout=20 self._create_instruments(instrument_config) 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) + def _make_client(self, connect: bool = True) -> Client: + 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): + 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. """ 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) -> Any: self.client.close_instrument(instrument_name) + 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 # 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: - 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.") + logger.info("Successfully remade instrument client.") retval = func(self, *args, **kwargs) return retval 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. @@ -831,7 +949,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 @@ -839,7 +959,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. @@ -847,9 +967,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) @@ -858,7 +978,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. @@ -876,13 +996,19 @@ 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: Optional[str] = None, + select_instruments: Optional[List[str]] = None, + ) -> None: """ Save instrument parameters to a JSON file in nested format. @@ -890,12 +1016,18 @@ 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) + 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. @@ -904,9 +1036,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): + def __getitem__(self, item: str) -> ProxyInstrument: return self.instruments[item] - diff --git a/instrumentserver/config.py b/src/instrumentserver/config.py similarity index 67% rename from instrumentserver/config.py rename to src/instrumentserver/config.py index f389e67..d177c87 100644 --- a/instrumentserver/config.py +++ b/src/instrumentserver/config.py @@ -3,18 +3,19 @@ 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 - -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 +from typing import IO + +import ruamel.yaml # 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'] + raise AttributeError('"gui" field cannot be None') + 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/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/src/instrumentserver/gui/__init__.py b/src/instrumentserver/gui/__init__.py new file mode 100644 index 0000000..7a20358 --- /dev/null +++ b/src/instrumentserver/gui/__init__.py @@ -0,0 +1,59 @@ +from typing import Optional + +from .. import ( + QtCore, + QtWidgets, + resource, # noqa: F401 +) + + +def getStyleSheet() -> Optional[str]: + f = QtCore.QFile(":/style.css") + 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) -> QtWidgets.QDialog: + dg = QtWidgets.QDialog() + dg.setWindowTitle("instrumentserver") + dg.setWindowFlag(QtCore.Qt.WindowType.WindowMinimizeButtonHint) + dg.setWindowFlag(QtCore.Qt.WindowType.WindowMaximizeButtonHint) + dg.widget = w + + css = getStyleSheet() + w.setStyleSheet(css) + + lay = QtWidgets.QVBoxLayout(dg) + lay.addWidget(w) + lay.setContentsMargins(0, 0, 0, 0) + dg.setLayout(lay) + + dg.show() + return dg + + +def widgetMainWindow( + w: QtWidgets.QWidget, name: str = "instrumentserver" +) -> QtWidgets.QMainWindow: + mw = QtWidgets.QMainWindow() + mw.setWindowTitle(name) + mw.setCentralWidget(w) + + css = getStyleSheet() + w.setStyleSheet(css) + + mw.show() + return mw + + +def keepSmallHorizontally(w: QtWidgets.QWidget) -> None: + w.setSizePolicy( + QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum + ) + ) diff --git a/instrumentserver/gui/base_instrument.py b/src/instrumentserver/gui/base_instrument.py similarity index 71% rename from instrumentserver/gui/base_instrument.py rename to src/instrumentserver/gui/base_instrument.py index 77bfa7c..5e76af5 100644 --- a/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 Any, Dict, List, Optional, cast from instrumentserver import QtCore, QtGui, QtWidgets @@ -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: str, + star: bool = False, + trash: bool = False, + showDelegate: bool = True, + element: Any = None, + ) -> None: super().__init__() self.name = name @@ -139,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() @@ -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: 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) @@ -231,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. @@ -246,20 +257,22 @@ 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]) - if not self._matches_any_pattern(objectName, self.itemsHide): - item = self.addItem(fullName=objectName, star=False, trash=False, element=obj) - if self._matches_any_pattern(objectName, self.itemsTrash): + objectName = ".".join([prefix, objectName]) + 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): # 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(): if prefix is not None: - submodName = '.'.join([prefix, submodName]) + 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. """ @@ -268,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** @@ -280,14 +295,13 @@ 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. :param fullName: The name of the parameter """ - path = fullName.split('.')[:-1] - paramName = fullName.split('.')[-1] + path = fullName.split(".")[:-1] parent = self smName = None @@ -297,31 +311,52 @@ 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, + cast( + "QtCore.Qt.MatchFlags", + QtCore.Qt.MatchFlag.MatchExactly + | QtCore.Qt.MatchFlag.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): - 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): - items = self.findItems(fullName, QtCore.Qt.MatchExactly | QtCore.Qt.MatchRecursive, 0) + def removeItem(self, fullName: str) -> None: + items = self.findItems( + fullName, + cast( + "QtCore.Qt.MatchFlags", + QtCore.Qt.MatchFlag.MatchExactly | QtCore.Qt.MatchFlag.MatchRecursive, + ), + 0, + ) if len(items) > 0: item = items[0] @@ -334,7 +369,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 @@ -342,10 +377,10 @@ 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): + def onItemTrashToggle(self, item: "ItemBase") -> None: assert isinstance(item, ItemBase) if item.trash: item.trash = False @@ -353,11 +388,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 +400,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) @@ -376,29 +412,31 @@ def __init__(self, sourceModel: InstrumentModelBase, parent: Optional[QtCore.QOb 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: @@ -406,17 +444,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. """ @@ -426,9 +464,11 @@ 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) -> 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 +482,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( # type: ignore[arg-type] + item, "trash", False + ): # item could be None when it's trashed and hidden return False return super().filterAcceptsRow(source_row, source_parent) @@ -458,31 +500,30 @@ 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'): + # 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() assert isinstance(model, InstrumentModelBase) 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 hasattr(leftItem, "star") and hasattr(rightItem, "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) class InstrumentTreeViewBase(QtWidgets.QTreeView): - #: Signal(ItemBase) #: emitted when this item got its trashed action triggered. itemTrashToggle = QtCore.Signal(ItemBase) @@ -491,7 +532,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: QtCore.QAbstractItemModel, + delegateColumns: Optional[List[int]] = None, + parent: Optional[QtWidgets.QWidget] = None, + ) -> None: super().__init__(parent=parent) # Indicates if a column is using delegates. @@ -509,37 +555,37 @@ 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. 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) - 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) 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. """ @@ -554,8 +600,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) @@ -566,60 +612,83 @@ 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(persistentIndex.row(), persistentIndex.column(), persistentIndex.parent()) - item = self.modelActual.itemFromIndex(modelIndex) - proxyIndex = self.model().mapFromSource(modelIndex) + modelIndex = self.modelActual.index( # type: ignore[union-attr] + persistentIndex.row(), + persistentIndex.column(), + persistentIndex.parent(), + ) + 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(persistentIndex.row(), x, persistentIndex.parent()) for x in - self.delegateColumns] - proxyDelegateIndexes = [self.model().mapFromSource(index) for index in delegateIndexes] + delegateIndexes = [ + self.modelActual.index( # type: ignore[union-attr] + persistentIndex.row(), x, persistentIndex.parent() + ) + for x in self.delegateColumns # type: ignore[union-attr] + ] + proxyDelegateIndexes = [ + self.model().mapFromSource(index) # type: ignore[union-attr] + for index in delegateIndexes + ] 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(i, 0) # Only items at column 0 hold children and model info - item0 = self.modelActual.itemFromIndex(self.model().mapToSource(index0)) + 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( # type: ignore[union-attr] + self.model().mapToSource(index0) # type: ignore[union-attr] + ) 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( # 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)) - index0 = self.model().mapFromSource(self.modelActual.indexFromItem(item0)) + index = self.model().mapFromSource( # type: ignore[union-attr] + self.modelActual.indexFromItem(item) # type: ignore[union-attr] + ) + index0 = self.model().mapFromSource( # type: ignore[union-attr] + self.modelActual.indexFromItem(item0) # type: ignore[union-attr] + ) if item0.showDelegate: self.openPersistentEditor(index) if item0.hasChildren(): 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. @@ -629,22 +698,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: @@ -661,27 +732,27 @@ 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)) @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) @@ -690,24 +761,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: Any, + attr: str, + itemType: type = ItemBase, + modelType: type = InstrumentModelBase, + proxyModelType: type = InstrumentSortFilterProxyModel, + viewType: type = InstrumentTreeViewBase, + callSignals: bool = True, + parent: Optional[QtWidgets.QWidget] = None, + **modelKwargs: Any, + ) -> None: super().__init__(parent=parent) # initializing variables @@ -736,7 +811,7 @@ def __init__(self, instrument, 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 """ @@ -751,9 +826,11 @@ 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): + def makeToolbar(self) -> QtWidgets.QToolBar: """ Creates the toolbar, override to add more buttons to the toolbar. """ @@ -764,7 +841,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() @@ -772,29 +849,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" + 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" + 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( @@ -808,36 +883,39 @@ 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] = {'item': item, 'star': item.star, 'trash': item.trash} - if item.hasChildren(): - fillChildren(item) + items[item.name] = { # type: ignore[union-attr] + "item": item, + "star": item.star, # type: ignore[union-attr] + "trash": item.trash, # type: ignore[union-attr] + } + 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) - 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/instrumentserver/gui/instruments.py b/src/instrumentserver/gui/instruments.py similarity index 58% rename from instrumentserver/gui/instruments.py rename to src/instrumentserver/gui/instruments.py index 3834e52..1a41ae0 100644 --- a/instrumentserver/gui/instruments.py +++ b/src/instrumentserver/gui/instruments.py @@ -1,22 +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 Any, Callable, Dict, Optional, Union, cast + +from qcodes import Instrument from instrumentserver.gui.misc import AlertLabelGreen -from qcodes import Parameter, Instrument -from . import parameters, keepSmallHorizontally -from .base_instrument import InstrumentDisplayBase, ItemBase, InstrumentModelBase, InstrumentTreeViewBase, DelegateBase -from .parameters import ParameterWidget, AnyInput, AnyInputForMethod -from .. import QtWidgets, QtCore, QtGui, DEFAULT_PORT +from .. import DEFAULT_PORT, QtCore, QtGui, QtWidgets 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 ..helpers import nestedAttributeFromString +from ..params import ParameterManager, ParameterTypes, parameterTypes, paramTypeFromName +from . import keepSmallHorizontally +from .base_instrument import ( + DelegateBase, + InstrumentDisplayBase, + InstrumentModelBase, + InstrumentTreeViewBase, + ItemBase, +) +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? @@ -38,8 +41,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 + ) -> None: super().__init__(parent) self.typeInput = typeInput @@ -49,19 +53,37 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None, self.nameEdit = QtWidgets.QLineEdit(self) lbl = QtWidgets.QLabel("Name:") - lbl.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + lbl.setAlignment( + 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.AlignRight | QtCore.Qt.AlignVCenter) + lbl.setAlignment( + 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.AlignRight | QtCore.Qt.AlignVCenter) + lbl.setAlignment( + cast( + "QtCore.Qt.Alignment", + QtCore.Qt.AlignmentFlag.AlignRight + | QtCore.Qt.AlignmentFlag.AlignVCenter, + ) + ) layout.addWidget(lbl, 0, 4) layout.addWidget(self.unitEdit, 0, 5) @@ -69,31 +91,46 @@ 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) + lbl.setAlignment( + cast( + "QtCore.Qt.Alignment", + QtCore.Qt.AlignmentFlag.AlignRight + | QtCore.Qt.AlignmentFlag.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.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + 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( + cast( + "QtCore.Qt.Alignment", + QtCore.Qt.AlignmentFlag.AlignRight + | QtCore.Qt.AlignmentFlag.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 +140,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) @@ -115,17 +151,19 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None, 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('') + 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"] # 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() @@ -135,23 +173,23 @@ 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) @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("") @@ -165,7 +203,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 @@ -194,13 +238,13 @@ 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: 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() @@ -212,19 +256,20 @@ 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. """ 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: str = "", **kwargs: Any) -> None: super().__init__(**kwargs) self.unit = unit @@ -235,23 +280,27 @@ 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(self, widget: QtWidgets.QWidget, option: QtWidgets.QStyleOptionViewItem, - index: QtCore.QModelIndex) -> QtWidgets.QWidget: + def createEditor( # type: ignore[override] + 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. """ 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: @@ -268,58 +317,89 @@ 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'), - "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() 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) -> None: + """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:]) + def updateParameter(self, bp: ParameterBroadcastBluePrint) -> None: + 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, + cast( + "QtCore.Qt.MatchFlags", + QtCore.Qt.MatchFlag.MatchExactly + | QtCore.Qt.MatchFlag.MatchRecursive, + ), + 0, + ) if len(item) == 0: - if fullName not in self.itemsHide: + if fullName not in self.itemsHide: # type: ignore[operator] 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. 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 + 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() @@ -335,7 +415,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) @@ -344,43 +429,54 @@ 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 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.") + except RuntimeError: + 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: Any, + parent: Optional[QtWidgets.QWidget] = None, + viewType: type = ParametersTreeView, + callSignals: bool = True, + **kwargs: Any, + ) -> None: + 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) - - def connectSignals(self): + 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) -> None: super().connectSignals() self.model.itemNewValue.connect(self.view.onItemNewValue) @@ -395,20 +491,25 @@ 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( # 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): - w = QtWidgets.QPushButton( - QtGui.QIcon(":/icons/delete.svg"), "", parent=widget) + 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 } """) @@ -421,7 +522,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) @@ -430,22 +536,21 @@ 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) class ProfilesManager(QtWidgets.QComboBox): - #: Signal() #: 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 @@ -456,7 +561,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() @@ -467,7 +572,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() @@ -481,8 +586,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: Optional[QtWidgets.QWidget] = None, + **kwargs: Any, + ) -> None: + super().__init__( + instrument, + parent=None, + viewType=ParameterManagerTreeView, + callSignals=False, + **kwargs, + ) self.profileManager = ProfilesManager(parent=self) self.addParam = AddParameterWidget(parent=self) layout = self.layout() @@ -492,7 +608,7 @@ def __init__(self, instrument: Union[ProxyInstrument, ParameterManager], parent= 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) @@ -500,7 +616,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() @@ -509,46 +625,49 @@ 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(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() - 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() @@ -557,7 +676,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: @@ -570,13 +689,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']) + 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() @@ -591,33 +711,41 @@ 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(self, widget: QtWidgets.QWidget, option: QtWidgets.QStyleOptionViewItem, - index: QtCore.QModelIndex) -> QtWidgets.QWidget: + 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') + 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 - self.clearAlertsAction = QtWidgets.QAction('Clear alerts') + self.clearAlertsAction = QtWidgets.QAction("Clear alerts") self.contextMenu.addSeparator() self.contextMenu.addAction(self.clearAlertsAction) @@ -627,24 +755,25 @@ def __init__(self, model, *args, **kwargs): class InstrumentMethods(InstrumentDisplayBase): - - def __init__(self, instrument, **kwargs): - if 'instrument' in kwargs: - del kwargs['instrument'] + def __init__(self, instrument: Any, **kwargs: Any) -> None: + 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 ----------------------------------- @@ -655,22 +784,27 @@ 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: Optional[QtWidgets.QWidget] = None, + **modelKwargs: Any, + ) -> None: 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 @@ -678,7 +812,7 @@ def __init__(self, ins: Union[ProxyInstrument, Instrument], parent=None, **model 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) @@ -688,9 +822,15 @@ 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) self.parametersList.view.resizeColumnToContents(1) self.methodsList.view.resizeColumnToContents(0) + + 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"): + model.stopListener() + super().closeEvent(event) diff --git a/instrumentserver/gui/misc.py b/src/instrumentserver/gui/misc.py similarity index 63% rename from instrumentserver/gui/misc.py rename to src/instrumentserver/gui/misc.py index 3d6ee29..9e7f6fb 100644 --- a/instrumentserver/gui/misc.py +++ b/src/instrumentserver/gui/misc.py @@ -1,59 +1,74 @@ -from typing import Optional, Tuple +from typing import Any, Optional, Tuple, cast -from .. import QtWidgets, QtGui, QtCore +from .. import QtCore, QtGui, QtWidgets 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.setAlignment( + 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) - self.setToolTip('no alerts') + 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') + 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: + + 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) class DetachedTab(QtWidgets.QMainWindow): - #: Signal(QtWidgets.QWidget) #: 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 @@ -65,12 +80,11 @@ 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) 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) @@ -79,16 +93,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) @@ -97,64 +111,70 @@ 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. """ - 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. + 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)) + 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) @@ -170,7 +190,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) @@ -179,43 +199,47 @@ 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) + 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.resize(0, 0) + closeButton = self._tabBar.tabButton( + index, QtWidgets.QTabBar.ButtonPosition.LeftSide + ) + 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. """ 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): + def onMoveTab(self, fromIndex: int, toIndex: int) -> None: widget = self.widget(fromIndex) icon = self.tabIcon(fromIndex) text = self.tabText(fromIndex) @@ -223,11 +247,13 @@ 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) # 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. @@ -236,7 +262,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) @@ -253,11 +279,20 @@ 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: 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/instrumentserver/gui/parameters.py b/src/instrumentserver/gui/parameters.py similarity index 77% rename from instrumentserver/gui/parameters.py rename to src/instrumentserver/gui/parameters.py index 6f75f0e..c1d7f15 100644 --- a/instrumentserver/gui/parameters.py +++ b/src/instrumentserver/gui/parameters.py @@ -1,24 +1,24 @@ import logging -import math import numbers -from typing import Any, Optional, List import re +from typing import Any, Callable, List, Optional, Tuple 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__) # 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): +def float_formater(val: Any) -> str: """ For displaying float numbers with scientific notation. """ @@ -49,8 +49,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: Optional[QtWidgets.QWidget] = None, + additionalWidgets: Optional[List[QtWidgets.QWidget]] = None, + ) -> None: super().__init__(parent) @@ -61,8 +65,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 +80,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)) @@ -86,8 +90,13 @@ def __init__(self, parameter: Parameter, parent=None, # 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 | 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 +150,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 [] @@ -163,32 +177,32 @@ def __init__(self, parameter: Parameter, parent=None, 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: - 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) - 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) @@ -199,19 +213,23 @@ 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() 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) @@ -224,38 +242,40 @@ 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()) - except Exception as e: + except Exception: ret = self.input.text() return ret 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: - 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): + 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: + except Exception: val = None if not isinstance(val, numbers.Number): @@ -267,21 +287,24 @@ def checkIfNumber(self, value: str): NumberInput { } """) - def value(self): + def value(self) -> Optional[numbers.Number]: try: value = eval(self.text()) - except: + except Exception: return None if isinstance(value, numbers.Number): return value 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: - 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 +315,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): + + 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. - 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,9 +339,8 @@ 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/instrumentserver/helpers.py b/src/instrumentserver/helpers.py similarity index 81% rename from instrumentserver/helpers.py rename to src/instrumentserver/helpers.py index dd2df0f..c4e2e89 100644 --- a/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 @@ -23,15 +22,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: @@ -47,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__}" @@ -62,7 +61,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 +75,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 +95,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 +103,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 @@ -132,9 +134,9 @@ 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('.') + parts = key.split(".") d = nested for part in parts[:-1]: d = d.setdefault(part, {}) @@ -142,13 +144,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: 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. @@ -171,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): @@ -181,4 +184,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/instrumentserver/log.py b/src/instrumentserver/log.py similarity index 52% rename from instrumentserver/log.py rename to src/instrumentserver/log.py index cc5230e..2c0b5ca 100644 --- a/instrumentserver/log.py +++ b/src/instrumentserver/log.py @@ -2,13 +2,14 @@ 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 typing import Callable, Optional -from . import QtGui, QtWidgets, QtCore +from . import QtCore, QtGui, QtWidgets @unique @@ -19,93 +20,108 @@ 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): + 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): - 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) + def emit(self, record: logging.LogRecord) -> None: + 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): """ 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): + + def __init__( + self, parent: Optional[QtWidgets.QWidget] = None, level: int = logging.INFO + ) -> None: 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) @@ -119,7 +135,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...? @@ -133,15 +149,17 @@ 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. """ # 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: @@ -150,12 +168,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: 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) @@ -167,7 +191,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) @@ -177,7 +201,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) @@ -187,12 +211,12 @@ def setupLogging(addStreamHandler=True, logFile=None, 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/instrumentserver/testing/__init__.py b/src/instrumentserver/monitoring/__init__.py similarity index 100% rename from instrumentserver/testing/__init__.py rename to src/instrumentserver/monitoring/__init__.py diff --git a/instrumentserver/monitoring/listener.py b/src/instrumentserver/monitoring/listener.py similarity index 66% rename from instrumentserver/monitoring/listener.py rename to src/instrumentserver/monitoring/listener.py index cd06e23..6e7a26c 100644 --- a/instrumentserver/monitoring/listener.py +++ b/src/instrumentserver/monitoring/listener.py @@ -3,16 +3,21 @@ import os.path from abc import ABC, abstractmethod from dataclasses import dataclass -from datetime import datetime, timedelta, timezone +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 pytz -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 @@ -22,11 +27,15 @@ logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) + class Listener(ABC): - def __init__(self, addresses: list): - self.addresses = addresses + def __init__(self, addresses: list) -> None: + self.addresses = addresses + + @abstractmethod + def listenerEvent(self, *args: Any, **kwargs: Any) -> None: ... - def run(self): + def run(self) -> None: # creates zmq subscriber at specified address logger.info(f"Connecting to {self.addresses}") @@ -49,7 +58,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") @@ -65,39 +74,40 @@ 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'], - 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): + def from_dict(cls, config_dict: Dict[str, Any]) -> "InfluxConfig": 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"], ) 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 @@ -108,28 +118,38 @@ 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) - 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: 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): - def __init__(self, influxConfig: InfluxConfig): +class InfluxListener(Listener): + def __init__(self, influxConfig: InfluxConfig) -> None: super().__init__(influxConfig.addresses) self.addresses = influxConfig.addresses @@ -145,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 @@ -163,19 +185,28 @@ 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 = ['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]): + +def checkCSVConfig(configInput: Dict[str, Any]) -> bool: # check if all fields are present in the config file csvField = ["addresses", "params", "csv_path"] @@ -185,18 +216,18 @@ def checkCSVConfig(configInput: Dict[str, Any]): return False return True -def get_timezone_info(timezone_name): + +def get_timezone_info(timezone_name: str) -> Optional[ZoneInfo]: 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 - -def startListener(): - parser = argparse.ArgumentParser(description='Starting the listener') +def startListener() -> None: + + parser = argparse.ArgumentParser(description="Starting the listener") parser.add_argument("-c", "--config") args = parser.parse_args() @@ -204,23 +235,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 + return # 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/instrumentserver/params.py b/src/instrumentserver/params.py similarity index 72% rename from instrumentserver/params.py rename to src/instrumentserver/params.py index 07de93f..41d204c 100644 --- a/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__) @@ -27,24 +26,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 +43,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 +52,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 @@ -82,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 @@ -123,7 +113,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 +121,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) @@ -148,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: @@ -170,44 +160,47 @@ 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: 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. + 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. + raise ValueError(f"{n} does not exist.") + 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: - param = self._get_param(param_name) + 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. @@ -223,20 +216,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): + def remove_parameter(self, param_name: str, cleanup: bool = True) -> None: 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() @@ -249,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) @@ -287,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): @@ -298,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`). @@ -311,16 +308,22 @@ 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): - 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 +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. @@ -344,16 +348,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 +376,19 @@ 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"] + ) -> 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. @@ -390,18 +403,18 @@ 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: 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) @@ -414,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. """ @@ -423,5 +436,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/instrumentserver/resource.py b/src/instrumentserver/resource.py similarity index 98% rename from instrumentserver/resource.py rename to src/instrumentserver/resource.py index b6611bd..d4fd9e9 100644 --- a/instrumentserver/resource.py +++ b/src/instrumentserver/resource.py @@ -1,1473 +1,1480 @@ -# -*- 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 74% rename from instrumentserver/serialize.py rename to src/instrumentserver/serialize.py index 0f7bc98..59c4554 100644 --- a/instrumentserver/serialize.py +++ b/src/instrumentserver/serialize.py @@ -68,28 +68,27 @@ 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]]] -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 +112,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 +160,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 +176,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,22 +187,24 @@ def fromParamDict(paramDict: Dict[str, Any], else: logger.info(f"[{k}] does not support setting, ignore.") + # 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 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 -def validateParamDict(params: Dict[str, Any]): +def validateParamDict(params: Dict[str, Any]) -> None: if isSimpleFormat(params): return @@ -205,19 +216,22 @@ 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']) + 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 +240,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 +270,25 @@ 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( + submod, # type: ignore[arg-type] + get=get, + addPrefix=f"{addPrefix + name}.", + simpleFormat=simpleFormat, + includeMeta=includeMeta, + ) + ) return ret @@ -289,8 +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): @@ -304,4 +324,3 @@ def _getObjectByName(name: str, if elt.name == name: return elt return None - 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 64% rename from instrumentserver/server/application.py rename to src/instrumentserver/server/application.py index cf3a8d2..83fcc0a 100644 --- a/instrumentserver/server/application.py +++ b/src/instrumentserver/server/application.py @@ -2,22 +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, List, Optional, Tuple, Union, cast 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__) @@ -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 @@ -45,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)) @@ -54,32 +51,41 @@ 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) 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))) + self.customContextMenuRequested.connect( + 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): - items = self.findItems(name, QtCore.Qt.MatchExactly | QtCore.Qt.MatchRecursive, 0) + def removeObject(self, name: str) -> None: + items = self.findItems( + name, + cast( + "QtCore.Qt.MatchFlags", + QtCore.Qt.MatchFlag.MatchExactly | QtCore.Qt.MatchFlag.MatchRecursive, + ), + 0, + ) if len(items) > 0: item = items[0] idx = self.indexOfTopLevelItem(item) self.takeTopLevelItem(idx) del item - def _processSelection(self): + def _processSelection(self) -> None: items = self.selectedItems() if len(items) == 0: return @@ -87,15 +93,19 @@ 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() 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,56 +113,56 @@ 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() - 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) + 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.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 +175,23 @@ 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: Optional[QtWidgets.QWidget] = None, + flags: Any = ( + QtCore.Qt.WindowType.CustomizeWindowHint + | QtCore.Qt.WindowType.WindowCloseButtonHint + ), + ) -> None: super().__init__(parent, flags) - tittleText = 'Create New Instrument' + tittleText = "Create New Instrument" self.setWindowTitle(tittleText) layout = QtWidgets.QVBoxLayout(self) @@ -187,23 +207,25 @@ 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) + 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 @@ -212,7 +234,7 @@ def onAcceptButton(self): @QtCore.Slot(str) -def onExceptionDialog(exception: str): +def onExceptionDialog(exception: str) -> None: """ Opens a dialog displaying an exception. @@ -222,17 +244,17 @@ 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") 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_() @@ -242,7 +264,16 @@ 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: 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 @@ -274,14 +305,16 @@ 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)) self.setHeaderLabels(self.cols) - self.basedInstrumentAction = QtWidgets.QAction(f'Create instrument based on this') - self.basedInstrumentAction.setShortcut('N') + self.basedInstrumentAction = QtWidgets.QAction( + "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) @@ -289,14 +322,18 @@ 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))) + self.customContextMenuRequested.connect( + lambda x: self.contextMenu.exec_(self.mapToGlobal(x)) # type: ignore[arg-type] + ) 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: @@ -305,26 +342,42 @@ 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) + 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, + ) -> 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, + 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 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,28 +388,42 @@ def addInstrumentToTree(self, fullInsType: str = 'InstrumentType', insName: str 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) - 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): + 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: 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): + def onRemoveInstrumentFromTree(self) -> None: """ Removes both the potential instrument from the widget and the presets from the config dictionary. """ @@ -364,16 +431,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)) @@ -389,6 +456,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() @@ -396,7 +464,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, **kwargs): + def __init__(self, cli: Client, guiConfig: dict, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.guiConfig = guiConfig @@ -410,38 +478,55 @@ 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, + ) -> 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_() @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: @@ -451,35 +536,52 @@ 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. """ 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): + 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(name=insName, instrument_class=insType, *args, **kwargs) + self.cli.find_or_create_instrument( # type: ignore[misc] + name=insName, instrument_class=insType, *args, **kwargs + ) self.newInstrumentCreated.emit() except Exception as e: self.newInstrumentFailed.emit(str(e)) @@ -490,12 +592,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, + ) -> None: 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 +613,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) @@ -532,45 +644,48 @@ def __init__(self, startServer: Optional[bool] = True, 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) - 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.setIconSize(QtCore.QSize(16, 16)) + self.toolBar = self.addToolBar("Tools") + 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) + 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) + 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) + 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.") @@ -586,46 +701,64 @@ def __init__(self, startServer: Optional[bool] = True, # 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): - if hasattr(self, 'stationServerThread'): + def closeEvent(self, event: Optional[QtGui.QCloseEvent]) -> None: + 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) - event.accept() + try: + self.client.ask(self.stationServer.SAFEWORD) + except Exception: + pass + + try: + self.client.disconnect() + except Exception: + pass + 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( - lambda: self.log('Server thread finished.', LogLevels.info) + 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: @@ -637,7 +770,12 @@ 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: Any, + insKwargs: Any, + ) -> None: """ Add an instrument to the station list. @@ -649,19 +787,24 @@ 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): + def removeInstrumentFromGui(self, name: str) -> None: """Remove an instrument from the station list.""" self.stationList.removeObject(name) del self._bluePrints[name] @@ -669,48 +812,58 @@ 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: + return self.stationList.clear() - for ins in self.client.list_instruments(): + try: + instruments = self.client.list_instruments() + except RuntimeError: + return + if not instruments: + return + for ins in instruments: bp = self.client.getBluePrint(ins) self.stationList.addInstrument(bp) 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).""" - 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: 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.""" + 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: 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. @@ -724,16 +877,18 @@ 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) + 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 @@ -748,13 +903,13 @@ def onTabDeleted(self, name: str) -> None: del self.instrumentTabsOpen[name] @QtCore.Slot(str, object, object, object) - def onFuncCalled(self, n, args, kw, ret): - if n == 'close_and_remove_instrument': + 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) @@ -762,7 +917,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] = {} @@ -770,7 +925,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) @@ -782,27 +937,28 @@ 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]) - self.tabs.addUnclosableTab(stationWidgets, 'Station') + self.tabs.addUnclosableTab(stationWidgets, "Station") # Toolbar. - self.toolBar = self.addToolBar('Tools') - self.toolBar.setIconSize(QtCore.QSize(16, 16)) + self.toolBar = self.addToolBar("Tools") + 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) + 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() @@ -812,12 +968,12 @@ 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) @@ -825,16 +981,16 @@ 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: + except Exception: pass insWidget = widgetClass(ins, parent=self, **kwargs) @@ -851,10 +1007,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 @@ -865,19 +1021,21 @@ class EmbeddedClient(QtClient): inside the server application.""" @QtCore.Slot(str) - def start(self, addr: str): - self.addr = "tcp://localhost:" + addr.split(':')[-1] + 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""" @@ -895,13 +1053,13 @@ 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 = '' + 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)}]" @@ -924,7 +1082,7 @@ def parameterToHtml(bp: ParameterBluePrint, headerLevel=None): return ret + var -def instrumentToHtml(bp: InstrumentModuleBluePrint): +def instrumentToHtml(bp: InstrumentModuleBluePrint) -> str: ret = f"""
{bp.name}