diff --git a/.github/workflows/code_quality.yml b/.github/workflows/code_quality.yml new file mode 100644 index 0000000..741a729 --- /dev/null +++ b/.github/workflows/code_quality.yml @@ -0,0 +1,78 @@ +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.12" + - run: uv lock --locked + + lint: + runs-on: ubuntu-latest + needs: lockfile + steps: + - uses: actions/checkout@v5 + - uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + python-version: "3.12" + - run: uv sync --locked --group dev + - run: uv run ruff check + + format: + runs-on: ubuntu-latest + needs: lockfile + steps: + - uses: actions/checkout@v5 + - uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + python-version: "3.12" + - run: uv sync --locked --group dev + - run: uv run ruff format --check + + typecheck: + runs-on: ubuntu-latest + needs: lockfile + steps: + - uses: actions/checkout@v5 + - uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + python-version: "3.12" + - run: uv sync --locked --group dev + - run: uv run mypy + + test: + runs-on: ubuntu-latest + needs: lockfile + steps: + - uses: actions/checkout@v5 + - uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + python-version: "3.12" + - 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.12" + - run: uv build diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml index 8b73059..9c161c6 100644 --- a/.github/workflows/sphinx.yml +++ b/.github/workflows/sphinx.yml @@ -18,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: labcore-docs - environment-file: environment-docs.yml - - - name: Install labcore 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 @@ -44,4 +40,4 @@ jobs: if: github.ref == 'refs/heads/main' with: github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: docs/build/html \ No newline at end of file + publish_dir: docs/build/html diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..cb4732c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,64 @@ +## 1. Think Before Coding + +**Don't assume. Don't hide confusion. Surface tradeoffs.** + +Before implementing: + +- State your assumptions explicitly. If uncertain, ask. +- If multiple interpretations exist, present them - don't pick silently. +- If a simpler approach exists, say so. Push back when warranted. +- If something is unclear, stop. Name what's confusing. Ask. + +## 2. Simplicity First + +**Minimum code that solves the problem. Nothing speculative.** + +- No features beyond what was asked. +- No abstractions for single-use code. +- No "flexibility" or "configurability" that wasn't requested. +- No error handling for impossible scenarios. +- If you write 200 lines and it could be 50, rewrite it. + +Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify. + +## 3. Surgical Changes + +**Touch only what you must. Clean up only your own mess.** + +When editing existing code: + +- Don't "improve" adjacent code, comments, or formatting. +- Don't refactor things that aren't broken. +- Match existing style, even if you'd do it differently. +- If you notice unrelated dead code, mention it - don't delete it. + +When your changes create orphans: + +- Remove imports/variables/functions that YOUR changes made unused. +- Don't remove pre-existing dead code unless asked. + +The test: Every changed line should trace directly to the user's request. + +## 4. Goal-Driven Execution + +**Define success criteria. Loop until verified.** + +Transform tasks into verifiable goals: + +- "Add validation" → "Write tests for invalid inputs, then make them pass" +- "Fix the bug" → "Write a test that reproduces it, then make it pass" +- "Refactor X" → "Ensure tests pass before and after" + +For multi-step tasks, state a brief plan: + +``` +1. [Step] → verify: [check] +2. [Step] → verify: [check] +3. [Step] → verify: [check] +``` + +Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. + +--- + +**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes. \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..15559fb --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +Treat @AGENTS.md the same way you would CLAUDE.md \ No newline at end of file diff --git a/README.md b/README.md index 580eb08..75c5c4c 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,92 @@ # labcore -Code for measuring and such. - -## installation -- clone from github -- make the right environment: use ``$ conda env create -n labcore --file environment.yml`` -- install into your measurement environment: - ``$ pip install -e `` -- you should then be able to import: - ``>>> import labcore`` - -## requirements -what other packages are needed will depend a lot on which tools from this package you'll be using. -In general, this will be used in a typical qcodes measurement environment. -The different submodules in this package should list in their documentation if additional packages are required. + +A Python toolkit for acquiring, processing, and analyzing data in a condensed matter / quantum information physics lab. + +labcore is designed to complement the [QCodes](https://qcodes.github.io/Qcodes/) ecosystem — it sits alongside QCodes instruments and parameters, adding a flexible sweep framework, structured HDF5 storage, and analysis tools. + +**[Get started in 15 minutes →](https://toolsforexperiments.github.io/labcore/first_steps/15_min_guide.html)** + +--- + +## What's inside + +- **Sweep framework** — compose parameter sweeps with `+` (sequential), `*` (zip), and `@` (nested) operators; decorate functions with `@recording` to produce structured records automatically +- **Structured data storage** — `DataDict` and `DDH5Writer` for writing and reading HDF5 data files; `find_data`, `load_as_xr`, and `load_as_df` for data discovery and loading +- **Fitting** — lmfit-based fitting framework with built-in fit functions (cosine, exponential, linear, exponentially decaying sine, and more) and xarray integration +- **Analysis base** — a lightweight framework for organizing, saving, and loading analysis artifacts (figures, datasets, parameters) + +--- + +## Installation + +labcore is not yet on PyPI. Install directly from GitHub: + +```bash +pip install git+https://github.com/toolsforexperiments/labcore.git +``` + +Or clone and install in editable mode: + +```bash +git clone https://github.com/toolsforexperiments/labcore.git +pip install -e labcore/ +``` + +Requires Python ≥ 3.11. + +--- + +## Quick example + +```python +from labcore.measurement import sweep_parameter, record_as, recording, dep, independent + +# Define what to record +@recording(dep('signal', ['frequency'])) +def measure(frequency): + return {'signal': my_instrument.read(frequency)} + +# Run a sweep and save to HDF5 +from labcore.measurement.storage import run_and_save_sweep +folder, _ = run_and_save_sweep( + sweep_parameter('frequency', range(100, 200)) @ measure, + data_dir='./data', + name='resonator_scan', +) + +# Load the result as xarray +from labcore.data.datadict_storage import load_as_xr +ds = load_as_xr(folder) +``` + +See the [15-minute guide](https://toolsforexperiments.github.io/labcore/first_steps/15_min_guide.html) for a full walkthrough. + +--- + +## Command-line tools + +| Command | Description | +|---|---| +| `autoplot` | Live plotting server for monitoring running measurements | +| `reconstruct-data` | Reconstruct HDF5 files from safe-write temporary data | + +--- + +## Development + +```bash +git clone https://github.com/toolsforexperiments/labcore.git +cd labcore +uv sync --group dev +uv run pytest test/ -v +``` + +--- + +## License + +MIT. See [LICENSE](LICENSE) for details. + +## Authors + +Wolfgang Pfaff and Marcos Frenkel. \ No newline at end of file diff --git a/environment-docs.yml b/environment-docs.yml deleted file mode 100644 index 6a141a0..0000000 --- a/environment-docs.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: labcore-docs -channels: - - conda-forge -dependencies: - - python=3.10 - - sphinx - - pydata-sphinx-theme - - myst-parser - - myst-nb - - pandoc - - ipykernel - - linkify-it-py - - h5py # Not a doc requirement but the pip installation fails. diff --git a/environment.yml b/environment.yml deleted file mode 100644 index 5ea0f19..0000000 --- a/environment.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: labcore -channels: - - conda-forge - - defaults -dependencies: - - python=3.10 - - jupyterlab - - jupyter_bokeh - - qcodes=0.44.1 - - bokeh - - pandas - - xarray - - matplotlib - - numpy=1.26.4 - - scipy - - scikit-learn - - seaborn - - lmfit - - h5py=3.10.0 - - xhistogram - - holoviews - - panel - - param - - hvplot - - versioningit - - qtpy - - pip - - gitpython - - watchdog - - pywavelets diff --git a/labcore/analysis/__init__.py b/labcore/analysis/__init__.py deleted file mode 100644 index bd33955..0000000 --- a/labcore/analysis/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .analysis_base import DatasetAnalysis -from .hvplotting import Node, ValuePlot, ComplexHist, plot_data -from .fit import Fit, FitResult diff --git a/labcore/instruments/opx/__init__.py b/labcore/instruments/opx/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/labcore/instruments/opx/config.py b/labcore/instruments/opx/config.py deleted file mode 100644 index c92c9a3..0000000 --- a/labcore/instruments/opx/config.py +++ /dev/null @@ -1,354 +0,0 @@ -import os -import logging -import json -from typing import Dict, Any, Optional - -import numpy as np - -from instrumentserver.helpers import nestedAttributeFromString - -from .machines import close_my_qm - -logger = logging.getLogger(__name__) - -# FIXME: Docstring incomplete -class QMConfig: - """ - Base class for a QMConfig class. The purpose of this class is to implement the real time changes of the - parameter manager with the OPX. We do this to always have the most up-to-date parameters from the - parameter manager and integration weights (which depend on the parameters in the parameter manager). - - By default, when a new config is generated this class will close any open QuantumMachines that are using the same - controllers that this config uses. To not do this pass False to close_other_machines in config(). - - The user should still manually write the config dictionary used for the specific physical setup that the - measurement is going to be performed but a few helper methods are implemented in the base class: two helper - methods to write integration weights and a method that creates and adds the integration weights to the config dict. - - If the constructor is overriden the new constructor should call the super constructor to pass the parameter manager - instance used. - - If the constructor is overriden because the parameter manager is not being used, the method config(self) also needs - to be overriden. - - To have the integration weights added automatically into the config dict, you have to implement config_(self). - config_(self) should return the python dictionary without the integration weights in it. - If this is the case, the already implemented config(self) method will add the integration weights when called and - return the python dictionary with the integration weights added. - - The add_integration_weights will go through the items in the pulses dictionary and add weights to any - pulse that has the following characteristics: - * The key of the pulse starts with the str 'readout'. If the first 7 characters of the key are not 'readout' - that pulse will be ignored. - * It needs to have the string '_pulse', present in its key. anything that comes before '_pulse' is - as the name of the pulse, anything afterwards gets ignored. - e.g. If my pulse name is: 'readout_short_pulse', 'readout_short' is taken as a unique pulse that requires - integration weights. If another pulse exists called: 'readout_short_pulse_something_else' - add_integration_weights will add the same integration weights as it did to 'readout_short'. - This is useful if you have 2 different pules of the same length that need the same integration weights. - * For each unique pulse there needs to be a parameter in the parameter manager with the unique pulse name - followed by len. This is where the length of the pulse will be taken to get the - integration weights. An underscore ('_') in the pulse name will be interpreted as a dot ('.') in the - parameter manager. - e.g. If I have a pulse with name 'readout_short_pulse', the pulse name is 'readout_short' and there should - be a parameter in the parameter manager called: 'readout.short.len' with the pulse length in it. - Any pulse that does not fulfil any of the three requirements will be ignored and will not have - integration weights. - - 3 integration weights will be created for each unique pulse, a flat integration weights - for a full demodulation, flat integration weights for a sliced demodulation and weighted integration weights. - The weights for the weighted integration will be loaded from the calibration files folder. - If no weights are found in the calibration folder for a pulse, flat ones will be used instead. - If old integration weights are found in the file, they will be deleted. - - :param params: The instance of the parameter manager where the length of the pulses are stored. - :param opx_address: The address of the OPX where the config is going to get used. - :param opx_port: The port of the OPX where the config is going to get used. - """ - - def __init__(self, params, opx_address: Optional[str] = None, opx_port: Optional[int] = None, cluster_name: Optional[str] = None, - octave=None) -> None: - self.params = params - self.opx_address = opx_address - self.opx_port = opx_port - self.octave = octave - self.cluster_name = cluster_name - - def __call__(self, *args, **kwargs) -> Dict[str, Any]: - return self.config() - - def config(self, close_other_machines: bool=False) -> Dict[str, Any]: - """ - Creates the config dictionary. - - :param close_other_machines: If True, closes any currently open qm in the opx that uses the same controllers - that this config is using. - :returns: The config dictionary. - """ - original = self.config_() - conf_with_weights = self.add_integration_weights(original) - with_random_wf = self.add_random_waveform(conf_with_weights) - if close_other_machines: - if self.opx_port is None or self.opx_address is None: - logger.warning(f'opx_port or opx_adress are empty, cannot close qm.') - else: - close_my_qm(with_random_wf, self.opx_address, self.opx_port) - return with_random_wf - - def config_(self) -> Dict[str, Any]: - raise NotImplementedError - - def add_random_waveform(self, conf): - """ - Adds a random waveform to the config dictionary. This is needed becaouse of a bug in the qm code that does not - close open QuantumMachines if the configuration is exactly the same. - """ - config_dict = conf.copy() - config_dict['waveforms']['random_wf'] = {'type': 'constant', - 'sample': np.random.rand()*0.1} - return config_dict - - def add_integration_weights(self, conf): - """ - Automatically add integration weights to the config dictionary. See module docstring for further explanation on - the rules for this to work. - """ - integration_weights_file = 'calibration_files/integration_weights.json' - - # Changes to True if old integration weights are found - deleted_weights = False - - # Used to not repeat missing file warning - no_file_warning = False - config_dict = conf.copy() - pulses = {} - - if os.path.exists(integration_weights_file): - with open(integration_weights_file) as json_file: - loaded_weights = json.load(json_file) - else: - loaded_weights = None - - # Go throguh pulses and check if they should have integration weights - for key in config_dict['pulses'].keys(): - if 'readout_' in key and '_pulse' in key: - path_to_param = key.split('_') - readout_pulse_name = '' - - # find the parameter name as it should be in the parameter manager. - # note: it may be nested under something else! - for k in path_to_param: - if k[:5] == 'pulse': - break # found it! continue... - else: - # add all the stuff it's nested in... - if len(readout_pulse_name) > 0: - readout_pulse_name += f'_{k}' - else: - readout_pulse_name += f"{k}" - - pulse = readout_pulse_name # don't ask... too lazy to find all instances. - readout_param_name = readout_pulse_name.replace('_', '.') - len_param_name = readout_param_name + '.len' - - # str with the name of the length of the pulse in the param manager - if self.params.has_param(len_param_name): - if readout_pulse_name not in pulses.keys(): - pulse_len = nestedAttributeFromString(self.params, len_param_name)() - - # Using the old integration weights style for the sliced weights because the OPX currently - # raises an exception when using the new ones. - flat = [(0.2, pulse_len)] - minus_flat = [(-0.2, pulse_len)] - flat_sliced = [0.2] * int(pulse_len//4) - mflat_sliced = [-0.2] * int(pulse_len//4) - empty = [(0.0, pulse_len)] - empty_sliced = [0.0] * int(pulse_len//4) - - pulses[pulse] = {} - pulses[pulse][pulse + '_cos'] = { - 'cosine': flat, - 'sine': empty - } - pulses[pulse][pulse + '_sin'] = { - 'cosine': empty, - 'sine': flat - } - pulses[pulse][pulse + '_minus_sin'] = { - 'cosine': empty, - 'sine': minus_flat - } - pulses[pulse][pulse + '_sliced_cos'] = { - 'cosine': flat_sliced, - 'sine': empty_sliced - } - pulses[pulse][pulse + '_sliced_sin'] = { - 'cosine': empty_sliced, - 'sine': flat_sliced - } - pulses[pulse][pulse + '_sliced_minus_sin'] = { - 'cosine': empty_sliced, - 'sine': mflat_sliced - } - - # Creating the variables for the weighted integration weights. - # If integrationg weights of the correct length are found on file, they get overwritten with - # the proper loaded weights. - pulse_weight_I = flat - pulse_weight_Q = flat - pulse_weight_m_I = flat - pulse_weight_m_Q = flat - - pulse_weight_empty = empty - - if loaded_weights is not None: - # Check if the current pulse has loaded integration weights - if any(pulse in weights for weights in loaded_weights): - pulse_weight_I_temp = loaded_weights[pulse + '_I'] - pulse_weight_Q_temp = loaded_weights[pulse + '_Q'] - - I_length = sum(i[1] for i in pulse_weight_I_temp) - Q_length = sum(i[1] for i in pulse_weight_Q_temp) - - # Check that they are the correct length. - if I_length == pulse_len and Q_length == pulse_len: - pulse_weight_I = pulse_weight_I_temp - pulse_weight_Q = pulse_weight_Q_temp - - pulse_weight_m_I = [(-1*pulseI[0], pulseI[1]) for pulseI in pulse_weight_I] - pulse_weight_m_Q = [(-1*pulseQ[0], pulseQ[1]) for pulseQ in pulse_weight_Q] - - pulse_weight_empty = [(0.0, pulse_weight_I[0][1])] * len(pulse_weight_I) - logging.info(f'Loaded weighted integration weights for {pulse}.') - else: - logging.info(f'Found old integration weights for {pulse}, deleting them from file.') - loaded_weights.pop(pulse + '_I') - loaded_weights.pop(pulse + '_Q') - deleted_weights = True - - else: - logging.info(f'No integration weights found for {pulse}, using flat weights.') - else: - if not no_file_warning: - no_file_warning = True - logging.info('Integration weights file not found, using flat weights.') - - pulses[pulse][pulse + '_weighted_cos'] = { - 'cosine': pulse_weight_I, - 'sine': pulse_weight_m_Q - } - pulses[pulse][pulse + '_weighted_sin'] = { - 'cosine': pulse_weight_Q, - 'sine': pulse_weight_I - } - pulses[pulse][pulse + '_weighted_minus_sin'] = { - 'cosine': pulse_weight_m_Q, - 'sine': pulse_weight_m_I - } - - # Assembling the dictionary that the config dict needs in each pulse for integration weights. - possible_integration_weights_per_pulse = {} # Dictionary with integration weights for this pulse. - for weights in pulses[pulse].keys(): - possible_integration_weights_per_pulse[weights] = weights - config_dict["pulses"][key]['integration_weights'] = possible_integration_weights_per_pulse - - # Assembling the 'integration_weights' dictionary. - integration_weights = {} - for pul, val in pulses.items(): - for integration_name, int_weight in val.items(): - integration_weights[integration_name] = int_weight - - config_dict['integration_weights'] = integration_weights - - - if loaded_weights is not None: - # Check that there are not old integration weights for pulses that don't exists anymore. - delete = [] - for weights in loaded_weights.keys(): - if weights[:-2] not in pulses.keys(): - delete.append(weights) - deleted_weights = True - - for old_weight in delete: - # Delete the weight from the file if these weights are not used. - loaded_weights.pop(old_weight) - - # Delete weights that should be deleted and save the weights without the old ones present. - if deleted_weights: - os.remove(integration_weights_file) - if len(loaded_weights) != 0: - with open(integration_weights_file, 'w') as file: - json.dump(loaded_weights, file) - - - return config_dict - - # The following are helper methods written by Quantum Machines to create integration weights - def _round_to_fixed_point_accuracy(self, x, base=2 ** -15): - """ - Written by Quantum Machines. - """ - - return np.round(base * np.round(np.array(x) / base), 20) - - def convert_full_list_to_list_of_tuples(self, integration_weights, N=100, accuracy=2 ** -15): - """ - Written by Quantum Machines. - - Converts a list of integration weights, in which each sample corresponds to a clock cycle (4ns), to a list - of tuples with the format (weight, time_to_integrate_in_ns). - Can be used to convert between the old format (up to QOP 1.10) to the new format introduced in QOP 1.20. - - :param integration_weights: A list of integration weights. - :param N: Maximum number of tuples to return. The algorithm will first create a list of tuples, - and then if it is - too long, it will run :func:`compress_integration_weights` on them. - :param accuracy: The accuracy at which to calculate the integration weights. Default is 2^-15, which is - the accuracy at which the OPX operates for the integration weights. - :type integration_weights: list[float] - :type N: int - :type accuracy: float - :return: List of tuples representing the integration weights - """ - integration_weights = self._round_to_fixed_point_accuracy(integration_weights, accuracy) - changes_indices = np.where(np.abs(np.diff(integration_weights)) > 0)[0].tolist() - prev_index = -1 - new_integration_weights = [] - for curr_index in (changes_indices + [len(integration_weights) - 1]): - constant_part = (integration_weights[curr_index].tolist(), round(4 * (curr_index - prev_index))) - new_integration_weights.append(constant_part) - prev_index = curr_index - - new_integration_weights = self.compress_integration_weights(new_integration_weights, N=N) - return new_integration_weights - - def compress_integration_weights(self, integration_weights, N=100): - """ - Written by Quantum Machines. - - Compresses the list of tuples with the format (weight, time_to_integrate_in_ns) to one with length < N. - Works by iteratively finding the nearest integration weights and combining them with a weighted average. - - :param integration_weights: The integration_weights to be compressed. - :param N: The maximum list length required. - :return: The compressed list of tuples representing the integration weights. - """ - while len(integration_weights) > N: - diffs = np.abs(np.diff(integration_weights, axis=0)[:, 0]) - min_diff = np.min(diffs) - min_diff_indices = np.where(diffs == min_diff)[0] - integration_weights = np.array(integration_weights) - times1 = integration_weights[min_diff_indices, 1] - times2 = integration_weights[min_diff_indices + 1, 1] - weights1 = integration_weights[min_diff_indices, 0] - weights2 = integration_weights[min_diff_indices + 1, 0] - integration_weights[min_diff_indices, 0] = (weights1 * times1 + weights2 * times2) / (times1 + times2) - integration_weights[min_diff_indices, 1] = times1 + times2 - integration_weights = np.delete(integration_weights, min_diff_indices + 1, 0) - integration_weights = list(zip(integration_weights.T[0].tolist(), - integration_weights.T[1].astype(int).tolist())) - - return integration_weights - - def configure_octave(self, qmm, qm): - raise NotImplementedError diff --git a/labcore/instruments/opx/machines.py b/labcore/instruments/opx/machines.py deleted file mode 100644 index 354fc92..0000000 --- a/labcore/instruments/opx/machines.py +++ /dev/null @@ -1,28 +0,0 @@ -try: - from qm.QuantumMachinesManager import QuantumMachinesManager -except: - from qm.quantum_machines_manager import QuantumMachinesManager - - -def close_my_qm(config, host, port): - """ - Helper function that closes any machines that is open in the OPT with host and port that uses any controller that - is present in the passed config. - - Parameters - ---------- - config - Config dictionary from which we are trying to open a QuantumMachine - host - The OPT host ip address - port - The OPT port - - """ - qmm = QuantumMachinesManager(host=host, port=port) - controllers = [con for con in config['controllers'].keys()] - open_qms = [qmm.get_qm(machine_id=machine_id) for machine_id in qmm.list_open_quantum_machines()] - for qm in open_qms: - for con in controllers: - if con in qm.list_controllers(): - qm.close() diff --git a/labcore/instruments/opx/mixer.py b/labcore/instruments/opx/mixer.py deleted file mode 100644 index 52348cf..0000000 --- a/labcore/instruments/opx/mixer.py +++ /dev/null @@ -1,606 +0,0 @@ -"""Tools for using (IQ) mixers with the QM OPX. - -Required packages/hardware: -- QM OPX incl python software -- SignalHound USB SA124B + driver (comes with qcodes) -""" -from functools import partial -from typing import List, Tuple, Optional, Any, Dict, Callable -from time import sleep -from datetime import datetime - -import numpy as np -from matplotlib import pyplot as plt -from scipy.optimize import minimize - -from ..qcodes_drivers.SignalHound.Spike import Spike -from ..qcodes_drivers.SignalCore.SignalCore_sc5511a import SignalCore_SC5511A -from qcodes.instrument_drivers.rohde_schwarz.SGS100A import RohdeSchwarz_SGS100A - -from qm import QuantumMachine, QuantumMachinesManager -from qm.qua import * - -from .config import QMConfig -from dataclasses import dataclass - - -class MixerCalibration: - """Class for performing IQ mixer calibration. - - We assume that we control the I and Q with a QM OPX, and monitor the output of the mixer with a - SignalHound spectrum analyzer. - Requires that independently a correctly specified configuration for the OPX is available. - - Parameters - ---------- - lo_frq - LO frequency in Hz - if_frq - IF frequency in Hz (can be positive or negative, depending on the sideband you want) - analyzer - SignalHound qcodes driver instance - qm - Quantum Machine instance (with config applied) - mixer_name - the name of mixer we're tuning, as given in the QM config - element_name - the name of the element thats playing the IQ waveform, as given in the QM config - pulse_name - the name of the (CW) pulse we're playing to tune the mixer, as given in the QM config - """ - - def __init__(self, lo_frq: float, if_frq: float, analyzer: Spike, - qm: QuantumMachine, mixer_name: str, element_name: str, pulse_name: str - ): - - self.lo_frq = lo_frq - self.if_frq = if_frq - self.analyzer = analyzer - self.qm = qm - self.mixer_name = mixer_name - self.element_name = element_name - self.pulse_name = pulse_name - self.do_plot = True - - self.analyzer.mode('ZS') - sleep(0.5) - - def play_wf(self) -> None: - """Play an infinite loop waveform on the OPX. - We're scaling the amplitude of the pulse used by 0.5. - """ - with program() as const_pulse: - with infinite_loop_(): - play(self.pulse_name * amp(0.5), self.element_name) - - _ = self.qm.execute(const_pulse) - - def setup_analyzer(self, f: float) -> None: - """Set up the analyzer to measure at the given frequency `f` and sweep time. - - Signalhound driver is automatically put in zero-span mode when called. - - Parameters - ---------- - f - frequency to measure at, in Hz - mode - set the mode for the spectrum analyzer - """ - self.analyzer.zs_fcenter(f) - sleep(1.0) - - def measure_leakage(self) -> float: - """Measure max. signal power at the LO frequency.""" - sleep(0.1) - return self.analyzer.zs_power() - - def measure_upper_sb(self) -> float: - """Measure max. signal power at LO frequency + IF frequency""" - sleep(0.1) - return self.analyzer.zs_power() - - def measure_lower_sb(self) -> float: - """Measure max. signal power at LO frequency - IF frequency""" - sleep(0.1) - return self.analyzer.zs_power() - - @staticmethod - def IQ_imbalance_correction(g, phi) -> List: - """returns in the IQ mixer correction matrix as exepcted by the QM mixer config. - - Parameters - ---------- - g - relative amplitude imbalance between I and Q channels - phi - relative phase imbalance between I and Q channels - """ - c = np.cos(phi) - s = np.sin(phi) - N = 1 / ((1 - g ** 2) * (2 * c ** 2 - 1)) - return [float(N * x) for x in [(1 - g) * c, (1 + g) * s, - (1 - g) * s, (1 + g) * c]] - - # TODO: properly implement the range for the COBYLA function to be bounded in - def _optimize2d(self, func, initial_guess, ranges, - cb_options=None, maxit=200): - - """ - Performs minimization through COBYLA algorithm. - It starts with an initial simplex (triangle in current 2D case). - The initial simplex is a regular triangle centered around 'initial_guess'. - The 'initial_ranges[0]' (which is side of square for 'scan2D') is the diameter of the circumscribing circle. - - Parameters - ---------- - func - initial_guess - initial_ranges - cb_options - maxit - - Returns - ------- - res (coordinates of the found minimum) - - """ - - if cb_options is None: - cb_options = dict() - - nit = 0 - - x, y, z = [], [], [] - - def cb(vec): - val = func(vec) - x.append(vec[0]) - y.append(vec[1]) - z.append(val) - - print(f'vector: {vec}, result: {val}, iteration: {len(y)}') - - try: - res = minimize(func, initial_guess, method='COBYLA', - callback=cb, options=dict(**cb_options, maxiter=maxit)) - nit = len(y) - - except KeyboardInterrupt: - res = np.array([x[-1], y[-1]]) - print('optimization stopped by user') - - return res, nit - - def _scan2d(self, func, center, ranges, steps, - title='', xtitle='', ytitle='', ztitle='Power'): - - xvals = center[0] + np.linspace(-ranges[0] / 2., ranges[0] / 2., steps) - yvals = center[1] + np.linspace(-ranges[1] / 2., ranges[1] / 2., steps) - xx, yy = np.meshgrid(xvals, yvals, indexing='ij') - zz = np.ones_like(xx) * np.nan - - try: - for k, x in enumerate(xvals): - for l, y in enumerate(yvals): - p = func(np.array([x, y])) - zz[k, l] = p - print(f'{p:5.0f}', end='') - print() - - except KeyboardInterrupt: - print('scan stopped by user.') - - if self.do_plot: - fig, ax = plt.subplots(1, 1, constrained_layout=True) - im = ax.pcolormesh(xx, yy, zz, shading='auto') - cb = fig.colorbar(im, ax=ax, shrink=0.5, pad=0.02) - ax.set_title(title + f" {datetime.now().isoformat()}", fontsize='small') - cb.set_label(ztitle) - ax.set_xlabel(xtitle) - ax.set_ylabel(ytitle) - plt.show() - - min_idx = np.argmin(zz.flatten()) - return np.array([xx.flatten()[min_idx], yy.flatten()[min_idx]], dtype=float) - - def lo_leakage(self, iq_offsets: np.ndarray) -> float: - """Set the I and Q DC offsets and measure the leakage power (in dBm). - - Parameters - ---------- - iq_offsets - array with 2 elements (I and Q offsets), with dtype = float - """ - self.qm.set_output_dc_offset_by_element( - self.element_name, 'I', iq_offsets[0]) - self.qm.set_output_dc_offset_by_element( - self.element_name, 'Q', iq_offsets[1]) - - power = self.measure_leakage() - return power - - def lo_leakage_scan(self, center: np.ndarray = np.array([0., 0.]), - ranges: Tuple = (0.5, 0.5), steps: int = 11) -> np.ndarray: - """Scan the I and Q DC offsets and measure the leakage at each point. - - if `MixerCalibration.do_plot` is `True` (default), then this generates a live plot of this measurement. - - Parameters - ---------- - center - center coordinate [I_of, Q_of] - ranges - scan range on I and Q - steps - how many steps (will be used for both I and Q) - Returns - ------- - np.ndarray - the I/Q offset coordinate at which the smallest leakage was found - """ - self.setup_analyzer(self.lo_frq) - res = self._scan2d(self.lo_leakage, - center=center, ranges=ranges, steps=steps, - title='Leakage scan', xtitle='I offset', - ytitle='Q offset') - return res - - def optimize_lo_leakage(self, initial_guess: np.ndarray = np.array([0., 0.]), - ranges: Tuple[float, float] = (0.1, 0.1), - cb_options: Optional[Dict[str, Any]] = None): - """Optimize the IQ DC offsets using COBYLA. - - The initial guess and ranges are used to specify the initial simplex - for the COBYLA algorithm. - initial guess is the starting point, and initial ranges are the distances - along the two coordinates for the remaining two vertices of the initial simplex. - - Parameters - ---------- - initial_guess - x0 of the COBYLA algorithm, an array with two elements, for I and Q offset - ranges - distance for I and Q vectors to complete the initial simplex - cb_options - Options to pass to the `scipy.optimize.minimize(method='COBYLA')`. - Will be passed via the `options` dictionary. - """ - - self.setup_analyzer(self.lo_frq) - res, nit = self._optimize2d(self.lo_leakage, - initial_guess, - ranges=ranges, - cb_options=cb_options) - return res, nit - - def sb_imbalance(self, imbalance: np.ndarray) -> float: - """Set mixer imbalance and measure the upper SB power. - - Parameters - ---------- - imbalance - values for relative amplitude and phase imbalance - - Returns - ------- - float - upper SB power [dBm] - - """ - mat = self.IQ_imbalance_correction(imbalance[0], imbalance[1]) - self.qm.set_mixer_correction( - self.mixer_name, int(self.if_frq), int(self.lo_frq), - tuple(mat)) - return self.measure_upper_sb() - - def sb_imbalance_scan(self, center: np.ndarray = np.array([0., 0.]), - ranges: Tuple = (0.5, 0.5), steps: int = 11) -> np.ndarray: - """Scan the relative amplitude and phase imbalance and measure the leakage at each point. - - if `MixerCalibration.do_plot` is `True` (default), then this generates a live plot of this measurement. - - Parameters - ---------- - center - center coordinate [amp imbalance, phase imbalance] - ranges - scan range on the two imbalances - steps - how many steps (will be used for both imbalances) - - Returns - ------- - np.ndarray - the imbalance coordinate at which the smallest leakage was found for the lower sideband - """ - self.setup_analyzer(self.lo_frq - self.if_frq) # LO - IF - res = self._scan2d(self.sb_imbalance, - center=center, ranges=ranges, steps=steps, - title='SB imbalance scan', - xtitle='g', ytitle='phi') - return res - - def optimize_sb_imbalance(self, initial_guess: np.ndarray = np.array([0., 0.]), - ranges: Tuple[float, float] = (0.05, 0.05), - cb_options: Optional[Dict[str, Any]] = None) -> np.ndarray: - """Optimize the mixer imbalances using COBYLA. - - The initial guess and ranges are used to specify the initial simplex - for the COBYLA algorithm. - initial guess is the starting point, and initial ranges are the distances - along the two coordinates for the remaining two vertices of the initial simplex. - - Parameters - ---------- - initial_guess - x0 of the COBYLA algorithm, an array with two elements, for rel. amp and phase imbalance - ranges - distance for amp/phase imbalance vectors to complete the initial simplex - cb_options - Options to pass to the `scipy.optimize.minimize(method='COBYLA')`. - Will be passed via the `options` dictionary. - """ - self.setup_analyzer(self.lo_frq - self.if_frq) # LO - IF - res, nit = self._optimize2d(self.sb_imbalance, - initial_guess, - ranges=ranges, - cb_options=cb_options) - return res, nit - - -@dataclass -class MixerConfig: - #: Quantum machines config object - qmconfig: QMConfig - #: OPX address - opx_address: str - #: OPX port - opx_port: str - #: OPX cluster_name - opx_cluster_name: str - #: spectrum analyzer - analyzer: Spike - #: the LO for the mixer - generator: SignalCore_SC5511A - #: param that holds the IF - if_param: Callable - #: param holding the dc offsets - offsets_param: Callable - #: param holding the imbalances - imbalances_param: Callable - #: name of the mixer in the opx config - mixer_name: str - #: element we play a constant pulse on - element_name: str - #: name of the pulse we play - pulse_name: str - #: method for calibrating the mixer - calibration_method: str = ' ' - #: power for the generator - generator_power: Optional[float] = None - #: options for scanning-based optimization, offsets - offset_scan_ranges: Tuple[float, float] = (0.01, 0.01) - offset_scan_steps: int = 11 - #: options for scanning-based optimization, imbalances - imbalance_scan_ranges: Tuple[float, float] = (0.01, 0.01) - imbalance_scan_steps: int = 11 - #: param that holds the LO frequency - lo_param: Optional[Callable] = None - #: parameter that holds the frequency - frequency_param: Optional[Callable] = None - # do you want to provide custom initial point? - # (doesn't affect scan2D) - opt2D_of_custom_init: bool = True - opt2D_imb_custom_init: bool = True - # do you want to do a larger scan (typically first calibration) or smaller scan (around already found point)? - # (doesn't affect scan2D) - opt2D_of_dia: str = 'large' - opt2D_imb_dia: str = 'large' - - -def calibrate_mixer(config: MixerConfig, - offset_scan_ranges=None, - offset_scan_steps=None, - imbalance_scan_ranges=None, - imbalance_scan_steps=None, - calibrate_offsets=True, - calibrate_imbalance=True, - max_step_size=0.05): - """ - Runs the entire mixer calibration for any mixer - """ - print("Ensure that effective path lengths before I and Q of mixer are same.") - print(f"Calibrating {config.mixer_name} by {config.calibration_method}...") - - # TODO: Should be configurable - config.analyzer.zs_ref_level(-20) - config.analyzer.zs_sweep_time(0.01) - config.analyzer.zs_ifbw_auto(0) - config.analyzer.zs_ifbw(1e5) - - # setup the generator frequency and its power - if config.lo_param is not None: - mixer_lo_freq = config.lo_param() - config.generator.frequency(mixer_lo_freq) - elif config.frequency_param is not None: - mixer_lo_freq = config.frequency_param() - config.if_param() #RF - IF when measuring on the upper sideband - config.generator.frequency(mixer_lo_freq) - else: - mixer_lo_freq = config.generator.frequency() - - # support for both SignalCore and R&S SGS - if hasattr(config.generator, 'output_status'): - config.generator.output_status(1) - elif hasattr(config.generator, 'on'): - config.generator.on() - - if config.generator_power is not None: - config.generator.power(config.generator_power) - - if config.opx_cluster_name is not None: - qmm = QuantumMachinesManager.QuantumMachinesManager(host=config.opx_address, - port=None, - cluster_name=config.opx_cluster_name) - else: - qmm = QuantumMachinesManager.QuantumMachinesManager(host=config.opx_address, - port=config.opx_port,) - - - qm = qmm.open_qm(config.qmconfig(), close_other_machines=False) - - try: - # initialize Mixer class object - cal = MixerCalibration(mixer_lo_freq, - config.if_param(), - config.analyzer, qm, - mixer_name=config.mixer_name, - element_name=config.element_name, - pulse_name=config.pulse_name, - ) - - # Call the appropriate calibration functions - # calibrate voltage offsets for LO leakage - if calibrate_offsets: - offsets = config.offsets_param() - cal.play_wf() - if config.calibration_method == 'scanning': - print("\nOffset calibration through: scanning \n") - if offset_scan_steps is None: - offset_scan_steps = config.offset_scan_steps - if offset_scan_ranges is None: - offset_scan_ranges = config.offset_scan_ranges - - print(f'Offsets: {offsets} Ranges: {offset_scan_ranges} \n') - - res_offsets = cal.lo_leakage_scan( - offsets, - ranges=offset_scan_ranges, - steps=offset_scan_steps, - ) - offsets = res_offsets.tolist() - else: - print("\nOffset calibration through: COBYLA \n") - - if config.opt2D_of_custom_init is True: - pass - else: - offsets = [0, 0] - - custom_of_range = config.offset_scan_ranges - - if config.opt2D_of_dia == 'large': - custom_of_range = [0.05, 0.05] - elif config.opt2D_of_dia == 'small': - custom_of_range = [0.001, 0.001] - elif config.opt2D_of_dia == 'custom': - pass - - print(f'Offsets: {offsets} Ranges: {custom_of_range} \n') - - res_offsets, nit = cal.optimize_lo_leakage( - offsets, - ranges=custom_of_range, - cb_options=dict(rhobeg=max_step_size, tol=1e-6, - disp=True, catol=1e-10) - ) - - if isinstance(res_offsets, np.ndarray): - offsets = res_offsets.tolist() - elif res_offsets.success and nit < 200: - offsets = res_offsets.x.tolist() - else: - print('Failed to converge. Use different initial values. \n') - return - - print(f'best values for offsets: {offsets} \n') - config.offsets_param(offsets) - print(f'verifying: {cal.lo_leakage(offsets)} \n') - - # calibrate imbalances for sideband suppression - if calibrate_imbalance: - imbalances = config.imbalances_param() - cal.play_wf() - if config.calibration_method == 'scanning': - print("\nImbalance calibration through: scanning \n") - if imbalance_scan_steps is None: - imbalance_scan_steps = config.imbalance_scan_steps - if imbalance_scan_ranges is None: - imbalance_scan_ranges = config.imbalance_scan_ranges - - print(f'Imbalances: {imbalances} Ranges: {imbalance_scan_ranges} \n') - - res_imbalances = cal.sb_imbalance_scan( - imbalances, - ranges=imbalance_scan_ranges, - steps=imbalance_scan_steps, - ) - imbalances = res_imbalances.tolist() - else: - print("\nOffset calibration through: COBYLA \n") - - if config.opt2D_imb_custom_init is True: - pass - else: - if config.generator.IDN().get('vendor') == 'Rohde&Schwarz': - imbalances = [0, 1.57] - else: - imbalances = [0, 0] - - custom_imb_range = config.imbalance_scan_ranges - - if config.opt2D_imb_dia == 'large': - custom_imb_range = [0.05, 0.05] - elif config.opt2D_imb_dia == 'small': - custom_imb_range = [0.001, 0.001] - elif config.opt2D_imb_dia == 'custom': - pass - - print(f'Imbalances: {imbalances} Ranges: {custom_imb_range} \n') - - res_imbalances, nit = cal.optimize_sb_imbalance( - imbalances, - ranges=custom_imb_range, - cb_options=dict(rhobeg=max_step_size, tol=1e-6, - disp=True, catol=1e-10), - ) - - if isinstance(res_imbalances, np.ndarray): - imbalances = res_imbalances.tolist() - elif res_imbalances.success and nit < 200: - imbalances = res_imbalances.x.tolist() - - else: - print('Failed to converge. Use different initial values. \n') - return - - print(f'best values for imbalance: {imbalances} \n') - config.imbalances_param(imbalances) - print(f'verifying: {cal.sb_imbalance(imbalances)} \n') - - finally: - qm.close() - -def mixer_of_step(config: MixerConfig, qm: QuantumMachine, di, dq): - new_i = config.offsets_param()[0] + di - new_q = config.offsets_param()[1] + dq - qm.set_output_dc_offset_by_element(config.element_name, 'I', new_i) - qm.set_output_dc_offset_by_element(config.element_name, 'Q', new_q) - config.offsets_param([new_i, new_q]) - - -def mixer_imb_step(config: MixerConfig, qm: QuantumMachine, dg, dp): - new_g = config.imbalances_param()[0] + dg - new_p = config.imbalances_param()[1] + dp - if config.lo_param is not None: - lof = config.lo_param() - elif config.frequency_param is not None: - lof = config.frequency_param() + config.if_param() - else: - lof = config.generator.frequency() - - qm.set_mixer_correction(config.mixer_name, - int(config.if_param()), - int(lof), - tuple(MixerCalibration.IQ_imbalance_correction(new_g, new_p))) - config.imbalances_param([new_g, new_p]) diff --git a/labcore/instruments/opx/sweep.py b/labcore/instruments/opx/sweep.py deleted file mode 100644 index 76539f3..0000000 --- a/labcore/instruments/opx/sweep.py +++ /dev/null @@ -1,399 +0,0 @@ -from typing import Callable, Dict, Generator, List, Optional -from functools import wraps -from dataclasses import dataclass -import time -import logging - -import numpy as np - -from qm.qua import * -try: - from qm.QuantumMachinesManager import QuantumMachinesManager -except: - from qm.quantum_machines_manager import QuantumMachinesManager - -from labcore.measurement import * -from labcore.measurement.record import make_data_spec -from labcore.measurement.sweep import AsyncRecord - -from .config import QMConfig - -# --- Options that need to be set by the user for the OPX to work --- -# config object that when called returns the config dictionary as expected by the OPX -config: Optional[QMConfig] = None # OPX config dictionary - -# WARNING: DO NOT TOUCH THIS VARIABLE. IT IS GLOBAL AND HANDLED BY THE CONTEXT MANAGER. -_qmachine_context = None - - -logger = logging.getLogger(__name__) - - -class QuantumMachineContext: - """ - Context manager for the Quantum Machine. It will open the machine when entering the context and close it when - exiting, after all measurement completed. This is used via a with statement, i.e.: - - ``` - with QuantumMachineContext() as qmc: - [your measurement code here] - ``` - - This does not need to be used, but if measurements are done repeatedly and precompiling with the OPX is - desired, it saves some time. - - Warning: Using a context manager doesn't let you update the config of the OPX. If you want to change the - config, you need to open a new quantum machine or use precompiled measurements to overwrite aspects of the - measurement on the fly. - """ - - def __init__(self, wf_overrides: Optional[Dict] = None, if_overrides: Optional[Dict] = None, *args, **kwargs): - """ - Initializes the context manager with a function to be executed, its arguments, and optional overrides. - - :param fun: The function to be executed in the quantum machine. - :param args: Positional arguments for the function. - :param wf_overrides: Optional dictionary of overrides for the waveforms. - :param if_overrides: Optional dictionary of overrides for the intermediate frequencies. - :param kwargs: Keyword arguments for the function. - """ - global config - self.wf_overrides = wf_overrides - self.if_overrides = if_overrides - self.kwargs = kwargs - - self._qmachine_mgr = None - self._qmachine = None - self._program_id = None - - def __enter__(self): - global config, _qmachine_context - _qmachine_mgr = QuantumMachinesManager( - host=config.opx_address, - port=config.opx_port, - cluster_name=config.cluster_name, - octave=config.octave - ) - - self._qmachine = _qmachine_mgr.open_qm(config(), close_other_machines=False) - _qmachine_context = self - return self - - def __exit__(self, exc_type, exc_value, traceback): - global _qmachine_context - if self._qmachine is not None: - self._qmachine.close() - _qmachine_context = None - - -@dataclass -class TimedOPXData(DataSpec): - def __post_init__(self): - super().__post_init__() - if self.depends_on is None or len(self.depends_on) == 0: - deps = [] - else: - deps = list(self.depends_on) - self.depends_on = [self.name+'_time_points'] + deps - -@dataclass -class ComplexOPXData(DataSpec): - i_data_stream: str = 'I' - q_data_stream: str = 'Q' - - -class RecordOPXdata(AsyncRecord): - """ - Implementation of AsyncRecord for use with the OPX machine. - """ - - def __init__(self, *specs): - self.communicator = {} - # self.communicator['raw_variables'] = [] - self.user_data = [] - self.specs = [] - for s in specs: - spec = make_data_spec(s) - self.specs.append(spec) - if isinstance(spec, TimedOPXData): - tspec = indep(spec.name + "_time_points") - self.specs.append(tspec) - self.user_data.append(tspec.name) - - def setup(self, fun, *args, **kwargs) -> None: - """ - Establishes connection with the OPX and starts the measurement. The config of the OPX is passed through - the module variable global_config. It saves the result handles and saves initial values to the communicator - dictionary. - """ - global _qmachine_context - self.communicator["self_managed"] = False - # Start the measurement in the OPX. - if _qmachine_context is None: - qmachine_mgr = QuantumMachinesManager( - host=config.opx_address, - port=config.opx_port, - cluster_name=config.cluster_name, - octave=config.octave - ) - - qmachine = qmachine_mgr.open_qm(config(), close_other_machines=False) - logger.info(f"current QM: {qmachine}, {qmachine.id}") - self.communicator["self_managed"] = True - else: - qmachine_mgr = _qmachine_context._qmachine_mgr - qmachine = _qmachine_context._qmachine - - job = qmachine.execute(fun(*args, **kwargs)) - result_handles = job.result_handles - - # Save the result handle and create initial parameters in the communicator used in the collector. - self.communicator['result_handles'] = result_handles - self.communicator['active'] = True - self.communicator['counter'] = 0 - self.communicator['manager'] = qmachine_mgr - self.communicator['qmachine'] = qmachine - self.communicator['qmachine_id'] = qmachine.id - - # FIXME change this such that we make sure that we have enough data on all handles - def _wait_for_data(self, batchsize: int) -> None: - """ - Waits for the opx to have measured more data points than the ones indicated in the batchsize. Also checks that - the OPX is still collecting data, when the OPX is no longer processing, turn communicator['active'] to False to - exhaust the collector. - - :param batchsize: Size of batch. How many data-points is the minimum for the sweep to get in an iteration. - e.g. if 5, _control_progress will keep running until at least 5 new data-points - are available for collection. - """ - - # When ready becomes True, the infinite loop stops. - ready = False - - # Collect necessary values from communicator. - res_handle = self.communicator['result_handles'] - counter = self.communicator['counter'] - - while not ready: - statuses = [] - processing = [] - for name, handle in res_handle: - current_datapoint = handle.count_so_far() - - # Check if the OPX is still processing. - if res_handle.is_processing(): - processing.append(True) - - # Check if enough data-points are available. - if current_datapoint - counter >= batchsize: - statuses.append(True) - else: - statuses.append(False) - - else: - # Once the OPX is done processing turn ready True and turn active False to exhaust the generator. - statuses.append(True) - processing.append(False) - - if not False in statuses: - ready = True - if not True in processing: - self.communicator['active'] = False - - def cleanup(self): - """ - Functions in charge of cleaning up any software tools that needs cleanup. - - Currently, manually closes the _qmachine in the OPT so that simultaneous measurements can occur. - """ - logger.info('Cleaning up') - - if self.communicator["self_managed"]: - open_machines = self.communicator["manager"].list_open_quantum_machines() - logger.info(f"currently open QMs: {open_machines}") - machine_id = self.communicator["qmachine_id"] - self.communicator["qmachine"].close() - logger.info(f"QM with ID {machine_id} closed.") - - self.communicator["qmachine"] = None - self.communicator["manager"] = None - - - - def collect(self, batchsize: int = 100) -> Generator[Dict, None, None]: - """ - Implementation of collector for the OPX. Collects new data-points from the OPX and yields them in a dictionary - with the names of the recorded variables as keywords and numpy arrays with the values. Raises ValueError if a - stream name inside the QUA program has a different name than a recorded variable and if the amount of recorded - variables and streams are different. - - :param batchsize: Size of batch. How many data-points is the minimum for the sweep to get in an iteration. - e.g. if 5, _control_progress will keep running until at least 5 new data-points - are available for collection. - """ - - # Get the result_handles from the communicator. - result_handle = self.communicator['result_handles'] - try: - while self.communicator['active']: - # Restart values for each iteration. - return_data = {} - counter = self.communicator['counter'] # Previous iteration data-point number. - first = True - available_points = 0 - ds: Optional[DataSpec] = None - - # Make sure that the result_handle is active. - if result_handle is None: - yield None - - # Waits until new data-points are ready to be gathered. - self._wait_for_data(batchsize) - - def get_data_from_handle(name, up_to): - if up_to == counter: - return None - handle = result_handle.get(name) - handle.wait_for_values(up_to) - data = np.squeeze(handle.fetch(slice(counter, up_to))['value']) - return data - - for i, ds in enumerate(self.specs): - if isinstance(ds, ComplexOPXData): - iname = ds.i_data_stream - qname = ds.q_data_stream - if i == 0: - available_points = result_handle.get(iname).count_so_far() - idata = get_data_from_handle(iname, up_to=available_points) - qdata = get_data_from_handle(qname, up_to=available_points) - if (qdata is None or idata is None): - print(f'qdata is: {qdata}') - print(f'idata is: {idata}') - print(f'available points is:{available_points}') - print(f'i is: {i}') - print(f'ds is: {ds}') - print(f'iname is: {iname}') - print(f'qname is: {qdata}') - print(f'am I active: {self.communicator["active"]}') - print(f'counter is: {self.communicator["counter"]}') - - if qdata is not None and idata is not None: - return_data[ds.name] = idata + 1j*qdata - - elif ds.name in self.user_data: - continue - - elif ds.name not in result_handle: - raise RuntimeError(f'{ds.name} specified but cannot be found in result handle.') - - else: - name = ds.name - if i == 0: - available_points = result_handle.get(name).count_so_far() - return_data[name] = get_data_from_handle(name, up_to=available_points) - - if isinstance(ds, TimedOPXData): - data = return_data[ds.name] - if data is not None: - tvals = np.arange(1, data.shape[-1]+1) - if len(data.shape) == 1: - return_data[name + '_time_points'] = tvals - elif len(data.shape) == 2: - return_data[name + '_time_points'] = np.tile(tvals, data.shape[0]).reshape(data.shape[0], -1) - else: - raise NotImplementedError('someone needs to look at data saving ASAP...') - - self.communicator['counter'] = available_points - yield return_data - - finally: - self.cleanup() - - -class RecordPrecompiledOPXdata(RecordOPXdata): - """ - Implementation of AsyncRecord for use with precompiled OPX programs. - - To pass either waveform or IF overrides, use the QuantumMachineContext and set the overrides as attributes. - The overrides must be passed as dictionaries. - - For the waveform overrides the keys are the names of the waveforms as defined in the OPX config file, and - the values are the new waveform arrays. For an arbitrary (as defined in the qmconfig) waveform to be overridable, - the waveform must have `"is_overridable": True` set. Constant waveforms do not need to be set as such: the override - will simply be a constant value. Other waveform types are not overridable. - - For the IF overrides the keys are the names of the elements as defined in the OPX config file, and - the values are the new intermediate frequencies in Hz. - - Usage example: - ``` - def create_readout_wf(amp): - wf_samples = [0.0] * int(params.q01.readout.short.buffer()) + [amp] * int( - params.q01.readout.short.len() - - 2 * params.q01.readout.short.buffer() - ) + [0.0] * int(params.q01.readout.short.buffer()) - return wf_samples - - def create_drive_wf(amp): - return amp - - with QuantumMachineContext() as qmc: - loc = measure_time_rabi() - - qmc.wf_overrides = { - "waveforms": { - f"q01_short_readout_wf": create_readout_wf(), - f"q01_square_pi_pulse_iwf": create_drive_wf() - } - } - qmc.if_overrides = { - "q01": 80e6, - "q01_readout": 80e6 - } - - loc = measure_time_rabi() - ``` - This will perform a time Rabi measurement, redefine the IF and waveforms of the drive and readout elements, - and then execute the same measurement with the new settings. - - There is no need to create a new quantum machine or recompile in between measurements. - """ - - def setup(self, fun, *args, **kwargs): - """ - Starts the measurement using a provided _program_id. Compilation only happens if the _program_id is None. - """ - global _qmachine_context - self.communicator["self_managed"] = False - - if _qmachine_context is None: - raise RuntimeError("No quantum machine manager or quantum machine found. " - "Please use a context manager for precompiled measurements.") - - if _qmachine_context._program_id is None: - _qmachine_context._program_id = _qmachine_context._qmachine.compile(fun(*args, **kwargs)) - if _qmachine_context.wf_overrides is not None: - print(f"Using waveform overrides: {_qmachine_context.wf_overrides}") - pending_job = _qmachine_context._qmachine.queue.add_compiled(_qmachine_context._program_id, overrides=_qmachine_context.wf_overrides) - else: - print("No waveform overrides provided, using default waveforms.") - pending_job = _qmachine_context._qmachine.queue.add_compiled(_qmachine_context._program_id) - - if _qmachine_context.if_overrides is not None: - print(f"Using IF overrides: {_qmachine_context.if_overrides}") - for element, frequency in _qmachine_context.if_overrides.items(): - _qmachine_context._qmachine.set_intermediate_frequency(element, frequency) - _qmachine_context.if_overrides = None - else: - print("No IF overrides provided, using default IFs.") - - job = pending_job.wait_for_execution() - result_handles = job.result_handles - - self.communicator["result_handles"] = result_handles - self.communicator["active"] = True - self.communicator["counter"] = 0 - self.communicator["manager"] = _qmachine_context._qmachine_mgr - self.communicator["qmachine"] = _qmachine_context._qmachine - self.communicator["qmachine_id"] = _qmachine_context._qmachine.id - diff --git a/labcore/instruments/qcodes_drivers/Keysight/Keysight_N9030B.py b/labcore/instruments/qcodes_drivers/Keysight/Keysight_N9030B.py deleted file mode 100644 index 9c833bc..0000000 --- a/labcore/instruments/qcodes_drivers/Keysight/Keysight_N9030B.py +++ /dev/null @@ -1,746 +0,0 @@ -# modify based on the qcodes driver for Keysight N9030B spectrum analyzer -""" -A driver to control the Keysight N9030B spectrum analyzer using pyVISA and qcodes - -@author: Pfafflab (UIUC): Xi Cao, Michael Mollenhauer - -""" - -from __future__ import annotations - -from typing import Any - -import numpy as np - -from qcodes.instrument import InstrumentChannel, VisaInstrument -from qcodes.parameters import ( - Parameter, - ParameterWithSetpoints, - ParamRawDataType, - create_on_off_val_mapping, -) -from qcodes.validators import Arrays, Enum, Ints, Numbers - - -class FrequencyAxis(Parameter): - def __init__( - self, - start: Parameter, - stop: Parameter, - npts: Parameter, - *args: Any, - **kwargs: Any, - ) -> None: - super().__init__(*args, **kwargs) - self._start: Parameter = start - self._stop: Parameter = stop - self._npts: Parameter = npts - - def get_raw(self) -> ParamRawDataType: - start_val = self._start() - stop_val = self._stop() - npts_val = self._npts() - assert start_val is not None - assert stop_val is not None - assert npts_val is not None - return np.linspace(start_val, stop_val, npts_val) - - -class Trace(ParameterWithSetpoints): - def __init__(self, number: int, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.instrument: ( - KeysightN9030BSpectrumAnalyzerMode | KeysightN9030BPhaseNoiseMode - ) - self.root_instrument: KeysightN9030B - - self.number = number - - def get_raw(self) -> ParamRawDataType: - return self.instrument._get_data(trace_num=self.number) - - -class KeysightN9030BSpectrumAnalyzerMode(InstrumentChannel): - """ - Spectrum Analyzer Mode for Keysight N9030B instrument. - """ - - def __init__(self, parent: KeysightN9030B, name: str, *arg: Any, **kwargs: Any): - super().__init__(parent, name, *arg, **kwargs) - - self._min_freq = -8e7 - self._valid_max_freq: dict[str, float] = { - "503": 3.7e9, - "508": 8.5e9, - "513": 13.8e9, - "526": 27e9, - "544": 44.5e9, - } - opt: str | None = None - for hw_opt_for_max_freq in self._valid_max_freq.keys(): - if hw_opt_for_max_freq in self.root_instrument._options(): - opt = hw_opt_for_max_freq - assert opt is not None - self._max_freq = self._valid_max_freq[opt] - - self.add_parameter( - name="average_count", - unit="", - get_cmd=":SENS:AVER:COUN?", - set_cmd=self._set_average_count, - get_parser=int, - vals=Numbers(1, 10000), - docstring="total average count for the trace", - ) - - self.add_parameter( - name="start", - unit="Hz", - get_cmd=":SENSe:FREQuency:STARt?", - set_cmd=self._set_start, - get_parser=float, - vals=Numbers(self._min_freq, self._max_freq - 10), - docstring="start frequency for the sweep", - ) - - self.add_parameter( - name="stop", - unit="Hz", - get_cmd=":SENSe:FREQuency:STOP?", - set_cmd=self._set_stop, - get_parser=float, - vals=Numbers(self._min_freq + 10, self._max_freq), - docstring="stop frequency for the sweep", - ) - - self.add_parameter( - name="center", - unit="Hz", - get_cmd=":SENSe:FREQuency:CENTer?", - set_cmd=self._set_center, - get_parser=float, - vals=Numbers(self._min_freq + 5, self._max_freq - 5), - docstring="Sets and gets center frequency", - ) - - self.add_parameter( - name="span", - unit="Hz", - get_cmd=":SENSe:FREQuency:SPAN?", - set_cmd=self._set_span, - get_parser=float, - vals=Numbers(10, self._max_freq - self._min_freq), - docstring="Changes span of frequency", - ) - - self.add_parameter( - name="npts", - get_cmd=":SENSe:SWEep:POINts?", - set_cmd=self._set_npts, - get_parser=int, - vals=Ints(1, 20001), - docstring="Number of points for the sweep", - ) - - self.add_parameter( - name="res_bandwidth", - label="Resolution bandwidth", - unit="Hz", - get_cmd=":SENSe:BANDwidth?", - set_cmd=":SENSe:BANDwidth {}", - get_parser=float, - docstring="get and set resolution bandwidth" - ) - - self.add_parameter( - name="sweep_time", - label="Sweep time", - get_cmd=":SENSe:SWEep:TIME?", - set_cmd=":SENSe:SWEep:TIME {}", - get_parser=float, - unit="s", - docstring="gets sweep time", - ) - - self.add_parameter( - name="amp_ref_level", - label="Reference level", - get_cmd=":DISP:WIND:TRAC:Y:RLEV?", - set_cmd=":DISP:WIND:TRAC:Y:RLEV {}", - get_parser=float, - unit="dBm", - docstring="set the top value on the y-axis", - ) - - self.add_parameter( - name="amp_scale", - label="Scale / Div", - get_cmd=":DISP:WIND:TRAC:Y:PDIV?", - set_cmd=":DISP:WIND:TRAC:Y:PDIV {}", - get_parser=float, - unit="dB", - docstring="set the scale per div on the y-axis", - ) - - self.add_parameter( - name="auto_sweep_time_enabled", - get_cmd=":SENSe:SWEep:TIME:AUTO?", - set_cmd=self._enable_auto_sweep_time, - val_mapping=create_on_off_val_mapping(on_val="ON", off_val="OFF"), - docstring="enables auto sweep time", - ) - - self.add_parameter( - name="auto_sweep_type_enabled", - get_cmd=":SENSe:SWEep:TYPE:AUTO?", - set_cmd=self._enable_auto_sweep_type, - val_mapping=create_on_off_val_mapping(on_val="ON", off_val="OFF"), - docstring="enables auto sweep type", - ) - - self.add_parameter( - name="sweep_type", - get_cmd=":SENSe:SWEep:TYPE?", - set_cmd=self._set_sweep_type, - val_mapping={ - "fft": "FFT", - "sweep": "SWE", - }, - docstring="Sets up sweep type. Possible options are 'fft' and 'sweep'.", - ) - - self.add_parameter( - name="freq_axis", - label="Frequency", - unit="Hz", - start=self.start, - stop=self.stop, - npts=self.npts, - vals=Arrays(shape=(self.npts.get_latest,)), - parameter_class=FrequencyAxis, - docstring="Creates frequency axis for the sweep from start, " - "stop and npts values.", - ) - - self.add_parameter( - name="trace", - label="Trace", - unit="dB", - number=1, - vals=Arrays(shape=(self.npts.get_latest,)), - setpoints=(self.freq_axis,), - parameter_class=Trace, - docstring="Gets trace data.", - ) - - def _set_average_count(self, val: int) -> None: - """ - Sets the total average count for the trace - """ - self.write(f":SENS:AVER:COUN {val}") - - def _set_start(self, val: float) -> None: - """ - Sets start frequency - """ - stop = self.stop() - if val >= stop: - raise ValueError( - f"Start frequency must be smaller than stop " - f"frequency. Provided start freq is: {val} Hz and " - f"set stop freq is: {stop} Hz" - ) - - self.write(f":SENSe:FREQuency:STARt {val}") - - start = self.start() - if abs(val - start) >= 1: - self.log.warning(f"Could not set start to {val} setting it to {start}") - - def _set_stop(self, val: float) -> None: - """ - Sets stop frequency - """ - start = self.start() - if val <= start: - raise ValueError( - f"Stop frequency must be larger than start " - f"frequency. Provided stop freq is: {val} Hz and " - f"set start freq is: {start} Hz" - ) - - self.write(f":SENSe:FREQuency:STOP {val}") - - stop = self.stop() - if abs(val - stop) >= 1: - self.log.warning(f"Could not set stop to {val} setting it to {stop}") - - def _set_center(self, val: float) -> None: - """ - Sets center frequency and updates start and stop frequencies if they - change. - """ - self.write(f":SENSe:FREQuency:CENTer {val}") - self.update_trace() - - def _set_span(self, val: float) -> None: - """ - Sets frequency span and updates start and stop frequencies if they - change. - """ - self.write(f":SENSe:FREQuency:SPAN {val}") - self.update_trace() - - def _set_npts(self, val: int) -> None: - """ - Sets number of points for sweep - """ - self.write(f":SENSe:SWEep:POINts {val}") - - def _enable_auto_sweep_time(self, val: str) -> None: - """ - Enables auto sweep time - """ - self.write(f":SENSe:SWEep:TIME:AUTO {val}") - - def _enable_auto_sweep_type(self, val: str) -> None: - """ - Enables auto sweep type - """ - self.write(f":SENSe:SWEep:TYPE:AUTO {val}") - - def _set_sweep_type(self, val: str) -> None: - """ - Sets sweep type - """ - self.write(f":SENSe:SWEep:TYPE {val}") - - def _get_data(self, trace_num: int) -> ParamRawDataType: - """ - Gets data from the measurement. - """ - self.root_instrument.timeout.set_to(1000) - data_str = self.ask( - f":READ:{self.root_instrument.measurement()}{trace_num}?" - ) - data = np.array(data_str.rstrip().split(",")).astype("float64") - trace_data = data[1::2] - return trace_data - - def update_trace(self) -> None: - """ - Updates start and stop frequencies whenever span of/or center frequency - is updated. - """ - self.start() - self.stop() - - def setup_swept_sa_sweep(self, start: float, stop: float, npts: int) -> None: - """ - Sets up the Swept SA measurement sweep for Spectrum Analyzer Mode. - """ - self.root_instrument.mode("SA") - if "SAN" in self.root_instrument._available_meas(): - self.root_instrument.measurement("SAN") - else: - raise RuntimeError( - "Swept SA measurement is not available on your " - "Keysight N9030B instrument with Spectrum " - "Analyzer mode." - ) - self.start(start) - self.stop(stop) - self.npts(npts) - - def autotune(self) -> None: - """ - Autotune quickly get to the most likely signal of interest, and - position it optimally on the display. - """ - self.write(":SENS:FREQuency:TUNE:IMMediate") - self.center() - -class KeysightN9030BSpectrumAnalyzerModeMarkers(InstrumentChannel): - """ - Controls the marker commands in the - Spectrum Analyzer Mode for Keysight N9030B instrument. - """ - - def __init__(self, parent: KeysightN9030B, name: str, marker_channel: str, *arg: Any, **kwargs: Any): - super().__init__(parent, name, *arg, **kwargs) - - if marker_channel not in list(np.arange(1,13,1)): - raise ValueError('only markers 1 through 12 are available') - - self.add_parameter( - name='mode', - get_cmd=f':CALC:MARK{marker_channel}:MODE?', - set_cmd=f':CALC:MARK{marker_channel}:MODE {{}}', - get_parser=str, - docstring="get and set current marker mode", - ) - self.add_parameter( - name='freq', - label="Frequency", - unit="Hz", - get_cmd=f':CALC:MARK{marker_channel}:X?', - set_cmd=f':CALC:MARK{marker_channel}:X {{}}', - get_parser=float, - docstring="get and set current marker frequency\ - if marker mode is POS or FIX, will return markers freq position; \ - if marker mode is DELT, will return the freq difference between the current \ - marker and the following numbered marker", - ) - self.add_parameter( - name='amp', - label='Power', - unit='dBm', - get_cmd=f':CALC:MARK{marker_channel}:Y?', - set_cmd=f':CALC:MARK{marker_channel}:Y {{}}', - get_parser=float, - docstring="get and set current marker amplitude; \ - if marker mode is POS or FIX, will return markers amp position; \ - if marker mode is DELT, will return the amp difference between the current \ - marker and the following numbered marker", - ) - - - - -class KeysightN9030BPhaseNoiseMode(InstrumentChannel): - """ - Phase Noise Mode for Keysight N9030B instrument. - """ - - def __init__(self, parent: KeysightN9030B, name: str, *arg: Any, **kwargs: Any): - super().__init__(parent, name, *arg, **kwargs) - - self._min_freq = 1 - self._valid_max_freq: dict[str, float] = { - "503": 3699999995, - "508": 8499999995, - "513": 13799999995, - "526": 26999999995, - "544": 44499999995, - } - opt: str | None = None - for hw_opt_for_max_freq in self._valid_max_freq.keys(): - if hw_opt_for_max_freq in self.root_instrument._options(): - opt = hw_opt_for_max_freq - assert opt is not None - self._max_freq = self._valid_max_freq[opt] - - self.add_parameter( - name="npts", - get_cmd=":SENSe:LPLot:SWEep:POINts?", - set_cmd=":SENSe:LPLot:SWEep:POINts {}", - get_parser=int, - vals=Ints(601, 20001), - docstring="Number of points for the sweep", - ) - - self.add_parameter( - name="start_offset", - unit="Hz", - get_cmd=":SENSe:LPLot:FREQuency:OFFSet:STARt?", - set_cmd=self._set_start_offset, - get_parser=float, - vals=Numbers(self._min_freq, self._max_freq - 10), - docstring="start frequency offset for the plot", - ) - - self.add_parameter( - name="stop_offset", - unit="Hz", - get_cmd=":SENSe:LPLot:FREQuency:OFFSet:STOP?", - set_cmd=self._set_stop_offset, - get_parser=float, - vals=Numbers(self._min_freq + 99, self._max_freq), - docstring="stop frequency offset for the plot", - ) - - self.add_parameter( - name="signal_tracking_enabled", - get_cmd=":SENSe:FREQuency:CARRier:TRACk?", - set_cmd=":SENSe:FREQuency:CARRier:TRACk {}", - val_mapping=create_on_off_val_mapping(on_val="ON", off_val="OFF"), - docstring="Gets/Sets signal tracking. When signal tracking is " - "enabled carrier signal is repeatedly realigned. Signal " - "Tracking assumes the new acquisition occurs repeatedly " - "without pause.", - ) - - self.add_parameter( - name="freq_axis", - label="Frequency", - unit="Hz", - start=self.start_offset, - stop=self.stop_offset, - npts=self.npts, - vals=Arrays(shape=(self.npts.get_latest,)), - parameter_class=FrequencyAxis, - docstring="Creates frequency axis for the sweep from " - "start_offset, stop_offset and npts values.", - ) - - self.add_parameter( - name="trace", - label="Trace", - unit="dB", - number=3, - vals=Arrays(shape=(self.npts.get_latest,)), - setpoints=(self.freq_axis,), - parameter_class=Trace, - docstring="Gets trace data.", - ) - - def _set_start_offset(self, val: float) -> None: - """ - Sets start offset for frequency in the plot - """ - stop_offset = self.stop_offset() - self.write(f":SENSe:LPLot:FREQuency:OFFSet:STARt {val}") - start_offset = self.start_offset() - - if abs(val - start_offset) >= 1: - self.log.warning( - f"Could not set start offset to {val} setting it to {start_offset}" - ) - if val >= stop_offset or abs(val - stop_offset) < 10: - self.log.warning( - f"Provided start frequency offset {val} Hz was " - f"greater than preset stop frequency offset " - f"{stop_offset} Hz. Provided start frequency " - f"offset {val} Hz is set and new stop freq offset" - f" is: {self.stop_offset()} Hz." - ) - - def _set_stop_offset(self, val: float) -> None: - """ - Sets stop offset for frequency in the plot - """ - start_offset = self.start_offset() - self.write(f":SENSe:LPLot:FREQuency:OFFSet:STOP {val}") - stop_offset = self.stop_offset() - - if abs(val - stop_offset) >= 1: - self.log.warning( - f"Could not set stop offset to {val} setting it to {stop_offset}" - ) - - if val <= start_offset or abs(val - start_offset) < 10: - self.log.warning( - f"Provided stop frequency offset {val} Hz was " - f"less than preset start frequency offset " - f"{start_offset} Hz. Provided stop frequency " - f"offset {val} Hz is set and new start freq offset" - f" is: {self.start_offset()} Hz." - ) - - def _get_data(self, trace_num: int) -> ParamRawDataType: - """ - Gets data from the measurement. - """ - raw_data = self.ask(f":READ:{self.root_instrument.measurement()}{1}?") - trace_res_details = np.array(raw_data.rstrip().split(",")).astype("float64") - - if len(trace_res_details) != 7 or ( - len(trace_res_details) >= 1 and trace_res_details[0] < -50 - ): - self.log.warning("Carrier(s) Incorrect or Missing!") - return -1 * np.ones(self.npts()) - - try: - data_str = self.ask( - f":READ:{self.root_instrument.measurement()}{trace_num}?" - ) - data = np.array(data_str.rstrip().split(",")).astype("float64") - except TimeoutError as e: - raise TimeoutError("Couldn't receive any data. Command timed out.") from e - - trace_data = data[1::2] - return trace_data - - def setup_log_plot_sweep( - self, start_offset: float, stop_offset: float, npts: int - ) -> None: - """ - Sets up the Log Plot measurement sweep for Phase Noise Mode. - """ - self.root_instrument.mode("PNOISE") - if "LPL" in self.root_instrument._available_meas(): - self.root_instrument.measurement("LPL") - else: - raise RuntimeError( - "Log Plot measurement is not available on your " - "Keysight N9030B instrument with Phase Noise " - "mode." - ) - - self.start_offset(start_offset) - self.stop_offset(stop_offset) - self.npts(npts) - - def autotune(self) -> None: - """ - On autotune, the measurement automatically searches for and tunes to - the strongest signal in the full span of the analyzer. - """ - self.write(":SENSe:FREQuency:CARRier:SEARch") - self.start_offset() - self.stop_offset() - - -class KeysightN9030B(VisaInstrument): - """ - Driver for Keysight N9030B PXA signal analyzer. Keysight N9030B PXA - signal analyzer is part of Keysight X-Series Multi-touch Signal - Analyzers. - This driver allows Swept SA measurements in Spectrum Analyzer mode and - Log Plot measurements in Phase Noise mode of the instrument. - - Args: - name - address - """ - - def __init__(self, name: str, address: str, timeout=None, **kwargs: Any, ) -> None: - super().__init__(name, address, timeout, terminator="\n", **kwargs) - - self.default_timout: float = 3600 - self._min_freq: float - self._max_freq: float - self._additional_wait: float = 1 - - self.add_parameter( - name="mode", - get_cmd=":INSTrument:SELect?", - set_cmd=":INSTrument:SELect {}", - vals=Enum(*self._available_modes()), - docstring="Allows setting of different modes present and licensed " - "for the instrument.", - ) - - self.add_parameter( - name="measurement", - get_cmd=":CONFigure?", - set_cmd=":CONFigure:{}", - vals=Enum("SAN", "LPL"), - docstring="Sets measurement type from among the available " - "measurement types.", - ) - - self.add_parameter( - name="cont_meas", - initial_value=False, - get_cmd=self._get_cont_meas, - set_cmd=self._enable_cont_meas, - get_parser=int, - vals = Ints(0, 1), - docstring="Enables or disables continuous measurement.", - ) - - self.add_parameter( - name="format", - get_cmd=":FORMat:TRACe:DATA?", - set_cmd=":FORMat:TRACe:DATA {}", - val_mapping={ - "ascii": "ASCii", - "int32": "INTeger,32", - "real32": "REAL,32", - "real64": "REAL,64", - }, - docstring="Sets up format of data received", - ) - - if "SA" in self._available_modes(): - sa_mode = KeysightN9030BSpectrumAnalyzerMode(self, name="sa") - self.add_submodule("sa", sa_mode) - for ch in list(np.arange(1,13,1)): - channel = KeysightN9030BSpectrumAnalyzerModeMarkers(self, name=f"sa_marker{ch}", marker_channel=ch) - self.add_submodule(f"sa_marker{ch}", channel) - else: - self.log.info("Spectrum Analyzer mode is not available on this instrument.") - - if "PNOISE" in self._available_modes(): - pnoise_mode = KeysightN9030BPhaseNoiseMode(self, name="pn") - self.add_submodule("pn", pnoise_mode) - else: - self.log.info("Phase Noise mode is not available on this instrument.") - self.connect_message() - - - def _get_cont_meas(self) -> int: - """ - Get the status of if the instrument is in contiuous measurement mode - """ - status = int(self.ask(":INITiate:CONTinuous?")) - return status - - - def _available_modes(self) -> tuple[str, ...]: - """ - Returns present and licensed modes for the instrument. - """ - available_modes = self.ask(":INSTrument:CATalog?") - av_modes = available_modes[1:-1].split(",") - modes: tuple[str, ...] = () - for i, mode in enumerate(av_modes): - if i == 0: - modes = modes + (mode.split(" ")[0],) - else: - modes = modes + (mode.split(" ")[1],) - return modes - - def _available_meas(self) -> tuple[str, ...]: - """ - Gives available measurement with a given mode for the instrument - """ - available_meas = self.ask(":CONFigure:CATalog?") - av_meas = available_meas[1:-1].split(",") - measurements: tuple[str, ...] = () - for i, meas in enumerate(av_meas): - if i == 0: - measurements = measurements + (meas,) - else: - measurements = measurements + (meas[1:],) - return measurements - - def _enable_cont_meas(self, val: str) -> None: - """ - Sets continuous measurement to ON or OFF. - """ - self.write(f":INITiate:CONTinuous {val}") - - def _options(self) -> tuple[str, ...]: - """ - Returns installed options numbers. - """ - options_raw = self.ask("*OPT?") - return tuple(options_raw[1:-1].split(",")) - - def reset(self) -> None: - """ - Reset the instrument by sending the RST command - """ - self.write("*RST") - - def abort(self) -> None: - """ - Aborts the measurement - """ - self.write(":ABORt") - - def ask_test(self, askstring) -> None: - """ - Directly sending a string for ask command - """ - data = self.ask(askstring) - return data - - def write_test(self, writestring) -> None: - """ - Directly sending a string for write command - """ - data = self.write(writestring) - return data diff --git a/labcore/instruments/qcodes_drivers/Keysight/Keysight_P937A.py b/labcore/instruments/qcodes_drivers/Keysight/Keysight_P937A.py deleted file mode 100644 index f035efa..0000000 --- a/labcore/instruments/qcodes_drivers/Keysight/Keysight_P937A.py +++ /dev/null @@ -1,530 +0,0 @@ -# -*- coding: utf-8 -*- -""" -A driver to control the Keysight VNA P9374A using pyVISA and qcodes - -@author: Hatlab: Ryan Kaufman; UIUC: Wolfgang Pfaff - -""" - -import logging -from typing import Any, Union, Dict, List, Tuple - -import numpy as np -from qcodes import (VisaInstrument, Parameter, ParameterWithSetpoints, InstrumentChannel, validators as vals) -from qcodes.instrument.parameter import ParamRawDataType - -""" -Some basic concepts for this VNA: - -Measurement: - The VNA object that actually holds the information of measurement of the device. It will not automatically - show up on the VNA software interface when created. -Trace: - The VNA object that can be shown on the VNA software, needs to link to a certain measurement. - As a working definition in our setting, we can think the trace and measurement are equivalent as there are always - created together. -Channel: - One channel can hold many traces. So that the common settings (frequency range, number of points, etc...) - can be applied to all the measurement. In this driver we are only use one channel. More needs to do if we want to - support multiple channel functions. -Window: - Place to show the selected traces on the VNA software. - -The settings that are independent to each trace/measurement (S-parameters, trace data) will be method/parameters for -Trace object while common settings for the whole channel will be method/parameters for the Keysight_P9374A_SingleChannel -object. - -For example: - vna.trace_1.s_parameter() - get the S-parameter for the first trace/measurement. - - vna.fstart() - get the start frequency for the whole channel. - -""" - - -class SParameterData(Parameter): - """ - SParameterData(Parameter) - - Qcodes parameter that can be used to set/get the S-parameter of this measurement. - - """ - - def __init__(self, trace_number: int, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.trace_number = trace_number - - def get_raw(self) -> ParamRawDataType: - _, traces, _ = self.root_instrument.get_existing_traces() - if self.instrument.npts() == 0 or self.trace_number not in traces: - return 'Trace is not on' - - data = self.root_instrument.ask(f":CALC1:MEAS{self.trace_number}:PAR?").strip('"') - return data - - def set_raw(self, S_parameter: str = 'S21') -> None: - _, traces, _ = self.root_instrument.get_existing_traces() - if self.instrument.npts() == 0 or self.trace_number not in traces: - return 'Trace is not on' - - self.root_instrument.write(f":CALC1:MEAS{self.trace_number}:PAR {S_parameter}") - - -class FrequencyData(Parameter): - """ - FrequencyData(Parameter) - - Qcodes parameter that can be used to get the frequency data of this measurment. - """ - - def __init__(self, trace_number: int, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.trace_number = trace_number - - def get_raw(self) -> ParamRawDataType: - _, traces, _ = self.root_instrument.get_existing_traces() - if self.instrument.npts() == 0 or self.trace_number not in traces: - return np.array([]) - - data = self.root_instrument.ask(f"CALC:MEAS{self.trace_number}:X?") - return np.array(data.split(',')).astype(float) - - -class TraceData(ParameterWithSetpoints): - """ - TraceData(ParameterWithSetpoints) - - Qcodes ParameterWithSetpoints that can be used to get the data of this measurement. - VNA will be set to polar format "POL" to acquire data by default. - If the VNA is in a different measurement format, it will be reset to that format after measurement, other wise VNA - will remain in format of self.data_fmt after the measurement. - - """ - - def __init__(self, trace_number: int, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.trace_number = trace_number - self.data_fmt = "POL" - - def get_raw(self) -> ParamRawDataType: - _, traces, _ = self.root_instrument.get_existing_traces() - if self.instrument.npts() == 0 or self.trace_number not in traces: - return np.array([]) - - # get the values of relevant parameters before taking the trace - prev_fmt = None - if self.data_fmt is not None: - prev_fmt = self.root_instrument.ask(f"CALC:MEAS{self.trace_number}:FORM?") - self.root_instrument.write(f"CALC:MEAS{self.trace_number}:FORM {self.data_fmt}") - - # Code will check if VNA average trpe is SWEEP. - # If not a type error will be raised. - try: - prev_trigger_source, prev_sweep_mode, prev_averaging = self.root_instrument.average() - data = self.root_instrument.ask(f"CALC:MEAS{self.trace_number}:DATA:FDATA?") - # set relevant parameters back to their old values - self.root_instrument.trigger_source(prev_trigger_source) - self.root_instrument.sweep_mode(prev_sweep_mode) - self.root_instrument.averaging(prev_averaging) - if prev_fmt is not None: - self.root_instrument.write(f"CALC:MEAS{self.trace_number}:FORM {prev_fmt}") - - # process complex data correctly - data = np.array(data.split(',')).astype(float) - if self.data_fmt in ['POL'] and data.size % 2 == 0: - data = data.reshape((int(data.size / 2), 2)) - data = data[:, 0] + 1j * data[:, 1] - return data - - except Exception as e: - print(f"Data taking failed. {type(e)}: {e.args}") - return np.zeros(self.instrument.npts()) - - -class Trace(InstrumentChannel): - """ - A Qcode InstrumentChannel creates from the parameter instrument: Keysight_P9374A_SingleChannel. - - This InstrumentChannel contains all the parameters and functions that are unique for each different - trace/measurement. - """ - - def __init__(self, parent: "Keysight_P9374A_SingleChannel", number: int, name: str, **kwargs: Any): - self._number = number - super().__init__(parent, name=name, **kwargs) - - self.add_parameter( - name='npts', - unit='', - get_cmd=self._get_npts, - docstring='number of points in the trace', - ) - - self.add_parameter( - name='frequency', - unit='Hz', - parameter_class=FrequencyData, - trace_number=self._number, - vals=vals.Arrays(shape=(self.npts.get_latest,)), - snapshot_exclude=True, - ) - - self.add_parameter( - name='data', - unit='', - setpoints=(self.frequency,), - parameter_class=TraceData, - trace_number=self._number, - vals=vals.Arrays(shape=(self.npts.get_latest,), - valid_types=(np.floating, np.complexfloating)), - snapshot_exclude=True, - ) - - self.add_parameter( - name='s_parameter', - unit='', - parameter_class=SParameterData, - trace_number=self._number, - vals=vals.Enum('S11', 'S12', 'S21', 'S22'), - get_parser=str - ) - - def _get_npts(self): - return len(self._get_xdata()) - - def _get_xdata(self) -> np.ndarray: - _, traces, _ = self.root_instrument.get_existing_traces() - if self._number not in traces: - return np.array([]) - data = self.ask(f"CALC:MEAS{self._number}:X?") - return np.array(data.split(',')).astype(float) - - -class Keysight_P9374A_SingleChannel(VisaInstrument): - """ - This is a very simple driver for the Keysight_P9374A Vector Network Analyzer - Performs basic manipulations of parameters and data acquisition - - Note: this version does not include a way of averaging via a BUS trigger - - """ - - def __init__(self, name, address=None, **kwargs): - - """ - Initializes the Keysight_P9374A, and communicates with the wrapper. - - Input: - name (string) : name of the instrument - address (string) : GPIB address - reset (bool) : resets to default values, default=False - """ - if address is None: - raise Exception('TCP IP address needed') - logging.info(__name__ + ' : Initializing instrument Keysight PNA') - - super().__init__(name, address, terminator='\n', **kwargs) - - self.write('CALC1:PAR:MNUM 1') # sets the active msmt to the first channel/trace - - # Add in parameters - self.add_parameter('fstart', - get_cmd=':SENS1:FREQ:STAR?', - set_cmd=':SENS1:FREQ:STAR {}', - vals=vals.Numbers(), - get_parser=float, - unit='Hz' - ) - self.add_parameter('fstop', - get_cmd=':SENS1:FREQ:STOP?', - set_cmd=':SENS1:FREQ:STOP {}', - vals=vals.Numbers(), - get_parser=float, - unit='Hz' - ) - self.add_parameter('fcenter', - get_cmd=':SENS1:FREQ:CENT?', - set_cmd=':SENS1:FREQ:CENT {}', - vals=vals.Numbers(), - get_parser=float, - unit='Hz' - ) - self.add_parameter('fspan', - get_cmd=':SENS1:FREQ:SPAN?', - set_cmd=':SENS1:FREQ:SPAN {}', - vals=vals.Numbers(), - get_parser=float, - unit='Hz' - ) - self.add_parameter('rfout', - get_cmd=':OUTP?', - set_cmd=':OUTP {}', - vals=vals.Ints(0, 1), - get_parser=int - ) - self.add_parameter('num_points', - get_cmd=':SENS1:SWE:POIN?', - set_cmd=':SENS1:SWE:POIN {}', - vals=vals.Ints(1, 100000), - get_parser=int - ) - self.add_parameter('ifbw', - get_cmd=':SENS1:BWID?', - set_cmd=':SENS1:BWID {}', - vals=vals.Numbers(10, 1.5e6), - get_parser=float) - self.add_parameter('power', - get_cmd=":SOUR1:POW?", - set_cmd=":SOUR1:POW {}", - unit='dBm', - get_parser=float, - vals=vals.Numbers(-85, 20) - ) - self.add_parameter('power_start', - get_cmd=':SOUR1:POW:STAR?', - set_cmd=':SOUR1:POW:STAR {}', - unit='dBm', - get_parser=float, - vals=vals.Numbers(-85, 10) - ) - self.add_parameter('power_stop', - get_cmd=':SOUR:POW:STOP?', - set_cmd=':SOUR1:POW:STOP {}', - unit='dBm', - get_parser=float, - vals=vals.Numbers(-85, 10)), - self.add_parameter('averaging', - get_cmd=':SENS1:AVER?', - set_cmd=':SENS1:AVER {}', - get_parser=int, - vals=vals.Ints(0, 1) - ) - # TODO: this throws an error currently. - # self.add_parameter('average_trigger', - # get_cmd=':TRIG:AVER?', - # set_cmd=':TRIG:AVER {}', - # get_parser=int, - # vals=vals.Ints(0, 1) - # ) - - self.add_parameter('avg_num', - get_cmd='SENS1:AVER:COUN?', - set_cmd='SENS1:AVER:COUN {}', - vals=vals.Ints(1), - get_parser=int - ) - self.add_parameter('avg_type', - get_cmd='SENS1:AVER:MODE?', - set_cmd='SENS1:AVER:MODE {}', - vals=vals.Enum('POIN', 'SWEEP'), - get_parser=str - ) - self.add_parameter('phase_offset', - get_cmd='CALC1:CORR:OFFS:PHAS?', - set_cmd='CALC1:CORR:OFFS:PHAS {}', - get_parser=float, - vals=vals.Numbers()) - self.add_parameter('electrical_delay', - get_cmd='CALC1:CORR:EDEL:TIME?', - set_cmd='CALC1:CORR:EDEL:TIME {}', - unit='s', - get_parser=float, - vals=vals.Numbers() - ) - self.add_parameter('trigger_source', - get_cmd='TRIG:SOUR?', - set_cmd='TRIG:SOUR {}', - vals=vals.Enum('IMM', 'EXT', 'MAN') - ) - self.add_parameter('sweep_mode', - get_cmd='SENS1:SWE:MODE?', - set_cmd='SENS1:SWE:MODE {}', - vals=vals.Enum('HOLD', - 'CONT', - 'GRO', - 'SING')) - self.add_parameter('trigger_mode', - get_cmd='SENS:SWE:TRIG:MODE?', - set_cmd='SENS:SWE:TRIG:MODE {}', - vals=vals.Enum('CHAN', 'SWE', 'POIN', 'TRAC') - ) - self.add_parameter('trform', - get_cmd=':CALC1:FORM?', - set_cmd=':CALC1:FORM {}', - vals=vals.Enum('MLOG', 'PHAS', - 'GDEL', - 'SCOM', 'SMIT', 'SADM', - 'POL', 'MLIN', - 'SWR', 'REAL', 'IMAG', - 'UPH', 'PPH', 'SLIN', 'SLOG', ) - ) - self.add_parameter('math', - get_cmd=':CALC1:MATH:FUNC?', - set_cmd=':CALC1:MATH:FUNC {}', - vals=vals.Enum('ADD', 'SUBT', 'DIV', 'MULT', 'NORM') - ) - self.add_parameter('sweep_type', - get_cmd=':SENS1:SWE:TYPE?', - set_cmd=':SENS1:SWE:TYPE {}', - vals=vals.Enum('LIN', 'LOG', 'SEGM', 'POW') - ) - self.add_parameter('correction', - get_cmd=':SENS1:CORR:STAT?', - set_cmd=':SENS1:CORR:STAT {}', - get_parser=int) - self.add_parameter('smoothing', - get_cmd=':CALC1:SMO:STAT?', - set_cmd=':CALC1:SMO:STAT {}', - get_parser=float - ) - self.add_parameter('sweep_time', - get_cmd=':SENS1:SWE:TIME?', - set_cmd=None, # generally just adjust ifbw and number of pts to change it, - get_parser=float, - unit='s' - ) - - for i in range(1, 17): - trace = Trace(self, number=i, name=f"trace_{i}") - self.add_submodule(f"trace_{i}", trace) - - self.connect_message() - - def clear_all_traces(self): - """remove all currently defined traces.""" - self.write("CALC:MEAS:DEL:ALL") - - def get_existing_traces_by_channel(self) -> Dict[int, List[Tuple[int, str]]]: - """Returns all currently available traces. - Assumes that traces/measurements do not have custom names not ending with the - measurement number. - - Returns - A dictionary, with keys being the channel indices that have traces in them. - values are tuples of trace/measurement number and parameter measured. - """ - ret = {} - for i in range(1, 9): - traces = self.ask(f"CALC{i}:PAR:CAT:EXT?").strip('"') - if traces == "NO CATALOG": - continue - else: - ret[i] = [] - traces = traces.split(',') - names = traces[::2] - params = traces[1::2] - for n, p in zip(names, params): - ret[i].append((int(n.split('_')[-1]), p)) - return ret - - def get_existing_traces(self) -> Tuple[List[int], List[int], List[str]]: - """ - Return three lists, with one item per current trace: channel, trace/measurement number, parameter - """ - chans, numbers, params = [], [], [] - trace_dict = self.get_existing_traces_by_channel() - for chan, traces in trace_dict.items(): - for number, param in traces: - chans.append(chan) - numbers.append(number) - params.append(param) - return chans, numbers, params - - def get_sweep_data(self): - """ - Gets stimulus data in displayed range of active measurement, returns array - Will return different data depending on sweep type. - - For example: - power sweep: 1xN array of powers in dBm - frequency sweep: 1xN array of freqs in Hz - Input: - None - Output: - sweep_values (Hz, dBm, etc...) - """ - logging.info(__name__ + ' : get stim data') - strdata = str(self.ask(':SENS1:X:VAL?')) - return np.array(list(map(float, strdata.split(',')))) - - def data_to_mem(self): - """ - Calls for data to be stored in memory - """ - logging.debug(__name__ + ": data to mem called") - self.write(":CALC1:MATH:MEM") - - def remove_trace(self, number: int): - """ - Remove selected trace - - Note that when removing a new trace, vna will restart average. - """ - _, traces, _ = self.get_existing_traces() - if number not in traces: - print('Trace does not exist. Nothing happens.') - else: - logging.debug(__name__ + f": remove trace{number}") - self.write(f"CALC1:MEAS{number}:DEL") - print('Trace is successfully removed.') - - def add_trace(self, number: int = 1, s_parameter: str = "S21"): - """ - Adds a trace with a specific s_parameter - - Note that when adding a new trace, vna will restart average. - """ - _, traces, _ = self.get_existing_traces() - if number in traces: - print('Trace exist. Please use another trace number or remove the current one with remove_trace(number).') - else: - logging.debug(__name__ + f": add trace{number} with S-parameter {s_parameter}") - self.write(f"CALC1:MEAS{number}:DEF '{s_parameter}'") - self.write(f"DISP:MEAS{number}:FEED 1") # always show this trace in the window 1 (FEED number). - print('Trace is successfully created.') - - # TODO: add timout protection - def average(self) -> Tuple[str, str, int]: - """ - Do the average self.avg_num() times. - - Read the trigger settings (trigger source and mode) before doing the average and return them. - During the average the VNA will be in manual trigger source with single trigger mode. - A single trigger signal is generated with 'INIT:IMM' for self.avg_num() times to complete the whole average - process. - - Note that after the average the VNA will remain in the trigger settings for the average process. - This is to give time for user to use another command to take the data from the trace/measurement. - - The trace.data() will automatically take the old settings and put them back. But you can also just take the - return values of this function and reset by yourself. - - Will check if VNA average type is in SWEEP. If not, a type error will be raised. - """ - - if self.avg_type() == 'POIN': - raise TypeError( - 'VNA average type is set to POINT, neeed to be SWEEP. Use vna.avg_type() function to change') - else: - prev_trigger_source = self.trigger_source() - prev_sweep_mode = self.sweep_mode() - prev_averaging = self.averaging() - # The following trigger settings are necessary for VNA to take the average - self.trigger_source('MAN') - self.sweep_mode('SING') - self.averaging(1) - self.write("SENS:AVER:CLE") # does not apply to point averaging - for i in range(self.avg_num()): - self.write('INIT:IMM') - averaged = 0 - while averaged == 0: - averaged = self.ask("*OPC?") - print('Average completed') - - return prev_trigger_source, prev_sweep_mode, prev_averaging - - def clear_averages(self) -> None: - """Reset averaging and wait for new trigger to start over.""" - self.write('SENS:AVER:CLE') diff --git a/labcore/instruments/qcodes_drivers/Keysight/__init__.py b/labcore/instruments/qcodes_drivers/Keysight/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/labcore/instruments/qcodes_drivers/Oxford/__init__.py b/labcore/instruments/qcodes_drivers/Oxford/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/labcore/instruments/qcodes_drivers/Oxford/triton.py b/labcore/instruments/qcodes_drivers/Oxford/triton.py deleted file mode 100644 index 50bbf97..0000000 --- a/labcore/instruments/qcodes_drivers/Oxford/triton.py +++ /dev/null @@ -1,621 +0,0 @@ -import configparser -import logging -import re -from functools import partial -from time import sleep -from typing import TYPE_CHECKING - -from qcodes.instrument import InstrumentBaseKWArgs, IPInstrument -from qcodes.validators import Enum, Ints, Numbers - -if TYPE_CHECKING: - from typing_extensions import Unpack - - from qcodes.parameters import Parameter - - -class OxfordTriton(IPInstrument): - r""" - Triton Driver - - Args: - name: name of the cryostat. - address: IP-address of the fridge computer. Defaults to None. - port: port of the oxford program running on the fridge computer. - The relevant port can be found in the manual for the fridge - or looked up on the fridge computer. Defaults to None. - terminator: Defaults to '\r\n' - tmpfile: an exported windows registry file from the registry - path: - `[HKEY_CURRENT_USER\Software\Oxford Instruments\Triton System Control\Thermometry]` - and is used to extract the available temperature channels. - timeout: Defaults to 20. - **kwargs: Forwarded to base class. - - Status: beta-version. - - Todo: - fetch registry directly from fridge-computer - - """ - - def __init__( - self, - name: str, - address: str | None = None, - port: int | None = None, - terminator: str = "\r\n", - tmpfile: str | None = None, - timeout: float = 20, - temp_channel_mapping: dict[str, str] = {}, - **kwargs: "Unpack[InstrumentBaseKWArgs]", - ): - super().__init__( - name, - address=address, - port=port, - terminator=terminator, - timeout=timeout, - **kwargs, - ) - - self._heater_range_auto = False - self._heater_range_temp = [0.03, 0.1, 0.3, 1, 12, 40] - self._heater_range_curr = [0.316, 1, 3.16, 10, 31.6, 100] - self._control_channel = 5 - self.pump_label_dict = {"TURB1": "Turbo 1", "COMP": "Compressor"} - - self.temp_channel_mapping = temp_channel_mapping - - self.magnet_available: bool = self._get_control_B_param("ACTN") != "INVALID" - """Indicates if a magnet is equipped *and* controlled by the Triton.""" - - self.time: Parameter = self.add_parameter( - name="time", - label="System Time", - get_cmd="READ:SYS:TIME", - get_parser=self._parse_time, - ) - """Parameter time""" - - self.action: Parameter = self.add_parameter( - name="action", - label="Current action", - get_cmd="READ:SYS:DR:ACTN", - get_parser=self._parse_action, - ) - """Parameter action""" - - self.status: Parameter = self.add_parameter( - name="status", - label="Status", - get_cmd="READ:SYS:DR:STATUS", - get_parser=self._parse_status, - ) - """Parameter status""" - - self.pid_control_channel: Parameter = self.add_parameter( - name="pid_control_channel", - label="PID control channel", - get_cmd=self._get_control_channel, - set_cmd=self._set_control_channel, - vals=Ints(1, 16), - ) - """Parameter pid_control_channel""" - - self.pid_mode: Parameter = self.add_parameter( - name="pid_mode", - label="PID Mode", - get_cmd=partial(self._get_control_param, "MODE"), - set_cmd=partial(self._set_control_param, "MODE"), - val_mapping={"on": "ON", "off": "OFF"}, - ) - """Parameter pid_mode""" - - self.pid_ramp: Parameter = self.add_parameter( - name="pid_ramp", - label="PID ramp enabled", - get_cmd=partial(self._get_control_param, "RAMP:ENAB"), - set_cmd=partial(self._set_control_param, "RAMP:ENAB"), - val_mapping={"on": "ON", "off": "OFF"}, - ) - """Parameter pid_ramp""" - - self.pid_setpoint: Parameter = self.add_parameter( - name="pid_setpoint", - label="PID temperature setpoint", - unit="K", - get_cmd=partial(self._get_control_param, "TSET"), - set_cmd=partial(self._set_control_param, "TSET"), - ) - """Parameter pid_setpoint""" - - self.pid_p: Parameter = self.add_parameter( - name="pid_p", - label="PID proportionality", - get_cmd=partial(self._get_control_param, "P"), - set_cmd=partial(self._set_control_param, "P"), - vals=Numbers(0, 1e3), - ) - """Parameter pid_p""" - - self.pid_i: Parameter = self.add_parameter( - name="pid_i", - label="PID intergral", - get_cmd=partial(self._get_control_param, "I"), - set_cmd=partial(self._set_control_param, "I"), - vals=Numbers(0, 1e3), - ) - """Parameter pid_i""" - - self.pid_d: Parameter = self.add_parameter( - name="pid_d", - label="PID derivative", - get_cmd=partial(self._get_control_param, "D"), - set_cmd=partial(self._set_control_param, "D"), - vals=Numbers(0, 1e3), - ) - """Parameter pid_d""" - - self.pid_rate: Parameter = self.add_parameter( - name="pid_rate", - label="PID ramp rate", - unit="K/min", - get_cmd=partial(self._get_control_param, "RAMP:RATE"), - set_cmd=partial(self._set_control_param, "RAMP:RATE"), - ) - """Parameter pid_rate""" - - self.pid_range: Parameter = self.add_parameter( - name="pid_range", - label="PID heater range", - # TODO: The units in the software are mA, how to - # do this correctly? - unit="mA", - get_cmd=partial(self._get_control_param, "RANGE"), - set_cmd=partial(self._set_control_param, "RANGE"), - vals=Enum(*self._heater_range_curr), - ) - """Parameter pid_range""" - - if self.magnet_available: - self.magnet_status: Parameter = self.add_parameter( - name="magnet_status", - label="Magnet status", - unit="", - get_cmd=partial(self._get_control_B_param, "ACTN"), - ) - """Parameter magnet_status""" - - self.magnet_sweeprate: Parameter = self.add_parameter( - name="magnet_sweeprate", - label="Magnet sweep rate", - unit="T/min", - get_cmd=partial(self._get_control_B_param, "RVST:RATE"), - set_cmd=partial(self._set_control_magnet_sweeprate_param), - ) - """Parameter magnet_sweeprate""" - - self.magnet_sweeprate_insta: Parameter = self.add_parameter( - name="magnet_sweeprate_insta", - label="Instantaneous magnet sweep rate", - unit="T/min", - get_cmd=partial(self._get_control_B_param, "RFST"), - ) - """Parameter magnet_sweeprate_insta""" - - self.B: Parameter = self.add_parameter( - name="B", - label="Magnetic field", - unit="T", - get_cmd=partial(self._get_control_B_param, "VECT"), - ) - """Parameter B""" - - self.Bx: Parameter = self.add_parameter( - name="Bx", - label="Magnetic field x-component", - unit="T", - get_cmd=partial(self._get_control_Bcomp_param, "VECTBx"), - set_cmd=partial(self._set_control_Bx_param), - ) - """Parameter Bx""" - - self.By: Parameter = self.add_parameter( - name="By", - label="Magnetic field y-component", - unit="T", - get_cmd=partial(self._get_control_Bcomp_param, "VECTBy"), - set_cmd=partial(self._set_control_By_param), - ) - """Parameter By""" - - self.Bz: Parameter = self.add_parameter( - name="Bz", - label="Magnetic field z-component", - unit="T", - get_cmd=partial(self._get_control_Bcomp_param, "VECTBz"), - set_cmd=partial(self._set_control_Bz_param), - ) - """Parameter Bz""" - - self.magnet_sweep_time: Parameter = self.add_parameter( - name="magnet_sweep_time", - label="Magnet sweep time", - unit="T/min", - get_cmd=partial(self._get_control_B_param, "RVST:TIME"), - ) - """Parameter magnet_sweep_time""" - else: - self.log.debug( - "Skipped adding magnet parameters. This may either be because there " - "is none equipped or because the Mercury iPS is not set to be " - "controlled by the Triton." - ) - - self.turb1_speed: Parameter = self.add_parameter( - name="turb1_speed", - label=self.pump_label_dict["TURB1"] + " speed", - unit="Hz", - get_cmd="READ:DEV:TURB1:PUMP:SIG:SPD", - get_parser=self._get_parser_pump_speed, - ) - """Parameter turb1_speed""" - - self._assign_named_temp_channels(self.temp_channel_mapping) - - self._add_pump_state() - self._add_temp_state() - self.chan_alias: dict[str, str] = {} - self.chan_temp_names: dict[str, dict[str, str | None]] = {} - if tmpfile is not None: - self._get_temp_channel_names(tmpfile) - self._get_temp_channels() - self._get_pressure_channels() - - try: - self._get_named_channels() - except Exception: - logging.warning("Ignored an error in _get_named_channels\n", exc_info=True) - - self.connect_message() - - def set_B(self, x: float, y: float, z: float, s: float) -> None: - if not self.magnet_available: - raise RuntimeError("Magnet not available") - if 0 < s <= 0.2: - self.write( - "SET:SYS:VRM:COO:CART:RVST:MODE:RATE:RATE:" - + str(s) - + ":VSET:[" - + str(x) - + " " - + str(y) - + " " - + str(z) - + "]\r\n" - ) - self.write("SET:SYS:VRM:ACTN:RTOS\r\n") - t_wait = self.magnet_sweep_time() * 60 + 10 - print("Please wait " + str(t_wait) + " seconds for the field sweep...") - sleep(t_wait) - else: - print("Warning: set magnet sweep rate in range (0 , 0.2] T/min") - - def _get_control_B_param(self, param: str) -> float | str | list[float] | None: - cmd = f"READ:SYS:VRM:{param}" - return self._get_response_value(self.ask(cmd)) - - def _get_control_Bcomp_param(self, param: str) -> float | str | list[float] | None: - cmd = f"READ:SYS:VRM:{param}" - return self._get_response_value(self.ask(cmd[:-2]) + cmd[-2:]) - - def _get_response(self, msg: str) -> str: - return msg.split(":")[-1] - - def _get_response_value(self, msg: str) -> float | str | list[float] | None: - msg = self._get_response(msg) - if msg.endswith("NOT_FOUND"): - return None - elif msg.endswith("IDLE"): - return "IDLE" - elif msg.endswith("RTOS"): - return "RTOS" - elif msg.endswith("Bx"): - return float(re.findall(r"[-+]?\d*\.\d+|\d+", msg)[0]) - elif msg.endswith("By"): - return float(re.findall(r"[-+]?\d*\.\d+|\d+", msg)[1]) - elif msg.endswith("Bz"): - return float(re.findall(r"[-+]?\d*\.\d+|\d+", msg)[2]) - elif len(re.findall(r"[-+]?\d*\.\d+|\d+", msg)) > 1: - return [ - float(re.findall(r"[-+]?\d*\.\d+|\d+", msg)[0]), - float(re.findall(r"[-+]?\d*\.\d+|\d+", msg)[1]), - float(re.findall(r"[-+]?\d*\.\d+|\d+", msg)[2]), - ] - try: - return float(re.findall(r"[-+]?\d*\.\d+|\d+", msg)[0]) - except Exception: - return msg - - def get_idn(self) -> dict[str, str | None]: - """Return the Instrument Identifier Message""" - idstr = self.ask("*IDN?") - idparts = [p.strip() for p in idstr.split(":", 4)][1:] - - return dict(zip(("vendor", "model", "serial", "firmware"), idparts)) - - def _get_control_channel(self, force_get: bool = False) -> int: - # verify current channel - if self._control_channel and not force_get: - tempval = self.ask(f"READ:DEV:T{self._control_channel}:TEMP:LOOP:MODE") - if not tempval.endswith("NOT_FOUND"): - return self._control_channel - - # either _control_channel is not set or wrong - for i in range(1, 17): - tempval = self.ask(f"READ:DEV:T{i}:TEMP:LOOP:MODE") - if not tempval.endswith("NOT_FOUND"): - self._control_channel = i - break - return self._control_channel - - def _set_control_channel(self, channel: int) -> None: - self._control_channel = channel - self.write(f"SET:DEV:T{self._get_control_channel()}:TEMP:LOOP:HTR:H1") - - def _get_control_param(self, param: str) -> float | str | list[float] | None: - chan = self._get_control_channel() - cmd = f"READ:DEV:T{chan}:TEMP:LOOP:{param}" - return self._get_response_value(self.ask(cmd)) - - def _set_control_param(self, param: str, value: float) -> None: - chan = self._get_control_channel() - cmd = f"SET:DEV:T{chan}:TEMP:LOOP:{param}:{value}" - self.write(cmd) - - def _set_control_magnet_sweeprate_param(self, s: float) -> None: - if 0 < s <= 0.2: - x = round(self.Bx(), 4) - y = round(self.By(), 4) - z = round(self.Bz(), 4) - self.write( - "SET:SYS:VRM:COO:CART:RVST:MODE:RATE:RATE:" - + str(s) - + ":VSET:[" - + str(x) - + " " - + str(y) - + " " - + str(z) - + "]\r\n" - ) - else: - print( - "Warning: set sweeprate in range (0 , 0.2] T/min, not setting sweeprate" - ) - - def _set_control_Bx_param(self, x: float) -> None: - s = self.magnet_sweeprate() - y = round(self.By(), 4) - z = round(self.Bz(), 4) - self.write( - "SET:SYS:VRM:COO:CART:RVST:MODE:RATE:RATE:" - + str(s) - + ":VSET:[" - + str(x) - + " " - + str(y) - + " " - + str(z) - + "]\r\n" - ) - self.write("SET:SYS:VRM:ACTN:RTOS\r\n") - # just to give an time estimate, +10s for overhead - t_wait = self.magnet_sweep_time() * 60 + 10 - print("Please wait " + str(t_wait) + " seconds for the field sweep...") - while self.magnet_status() != "IDLE": - pass - - def _set_control_By_param(self, y: float) -> None: - s = self.magnet_sweeprate() - x = round(self.Bx(), 4) - z = round(self.Bz(), 4) - self.write( - "SET:SYS:VRM:COO:CART:RVST:MODE:RATE:RATE:" - + str(s) - + ":VSET:[" - + str(x) - + " " - + str(y) - + " " - + str(z) - + "]\r\n" - ) - self.write("SET:SYS:VRM:ACTN:RTOS\r\n") - # just to give an time estimate, +10s for overhead - t_wait = self.magnet_sweep_time() * 60 + 10 - print("Please wait " + str(t_wait) + " seconds for the field sweep...") - while self.magnet_status() != "IDLE": - pass - - def _set_control_Bz_param(self, z: float) -> None: - s = self.magnet_sweeprate() - x = round(self.Bx(), 4) - y = round(self.By(), 4) - self.write( - "SET:SYS:VRM:COO:CART:RVST:MODE:RATE:RATE:" - + str(s) - + ":VSET:[" - + str(x) - + " " - + str(y) - + " " - + str(z) - + "]\r\n" - ) - self.write("SET:SYS:VRM:ACTN:RTOS\r\n") - # just to give an time estimate, +10s for overhead - t_wait = self.magnet_sweep_time() * 60 + 10 - print("Please wait " + str(t_wait) + " seconds for the field sweep...") - while self.magnet_status() != "IDLE": - pass - - def _get_named_channels(self) -> None: - allchans_str = self.ask("READ:SYS:DR:CHAN") - allchans = allchans_str.replace("STAT:SYS:DR:CHAN:", "", 1).split(":") - for ch in allchans: - msg = f"READ:SYS:DR:CHAN:{ch}" - rep = self.ask(msg) - if "INVALID" not in rep and "NONE" not in rep: - alias, chan = rep.split(":")[-2:] - self.chan_alias[alias] = chan - self.add_parameter( - name=alias, - unit="K", - get_cmd=f"READ:DEV:{chan}:TEMP:SIG:TEMP", - get_parser=self._parse_temp, - ) - - def _get_pressure_channels(self) -> None: - chan_pressure_list = [] - for i in range(1, 7): - chan = f"P{i}" - chan_pressure_list.append(chan) - self.add_parameter( - name=chan, - unit="bar", - get_cmd=f"READ:DEV:{chan}:PRES:SIG:PRES", - get_parser=self._parse_pres, - ) - self.chan_pressure = set(chan_pressure_list) - - def _get_temp_channel_names(self, file: str) -> None: - config = configparser.ConfigParser() - with open(file, encoding="utf16") as f: - next(f) - config.read_file(f) - - for section in config.sections(): - options = config.options(section) - namestr = '"m_lpszname"' - if namestr in options: - chan_number = int(re.findall(r"\d+", section)[-1]) + 1 - # the names used in the register file are base 0 but the api and the gui - # uses base one names so add one - chan = "T" + str(chan_number) - name = config.get(section, '"m_lpszname"').strip('"') - self.chan_temp_names[chan] = {"name": name, "value": None} - - def _assign_named_temp_channels(self, temp_channel_mapping: dict[str, str]) -> None: - temp_channel_mapping = dict(temp_channel_mapping) - for chan in temp_channel_mapping.keys(): - self.add_parameter( - name=temp_channel_mapping[chan], - unit="K", - get_cmd=f"READ:DEV:{chan}:TEMP:SIG:TEMP", - get_parser=self._parse_temp, - ) - - def _get_temp_channels(self) -> None: - chan_temps_list = [] - for i in range(1, 17): - chan = f"T{i}" - chan_temps_list.append(chan) - self.add_parameter( - name=chan, - unit="K", - get_cmd=f"READ:DEV:{chan}:TEMP:SIG:TEMP", - get_parser=self._parse_temp, - ) - self.chan_temps = set(chan_temps_list) - - def _parse_action(self, msg: str) -> str: - """Parse message and return action as a string - - Args: - msg: message string - Returns - action: string describing the action - - """ - action = msg[17:] - if action == "PCL": - action = "Precooling" - elif action == "EPCL": - action = "Empty precool loop" - elif action == "COND": - action = "Condensing" - elif action == "NONE": - if self.MC.get() < 2: - action = "Circulating" - else: - action = "Idle" - elif action == "COLL": - action = "Collecting mixture" - else: - action = "Unknown" - return action - - def _parse_status(self, msg: str) -> str: - return msg[19:] - - def _parse_time(self, msg: str) -> str: - return msg[14:] - - def _parse_temp(self, msg: str) -> float | None: - if "NOT_FOUND" in msg: - return None - return float(msg.split("SIG:TEMP:")[-1].strip("K")) - - def _parse_pres(self, msg: str) -> float | None: - if "NOT_FOUND" in msg: - return None - return float(msg.split("SIG:PRES:")[-1].strip("mB")) * 1e3 - - def _recv(self) -> str: - return super()._recv().rstrip() - - def _add_pump_state(self) -> None: - self.pumps = set(self.pump_label_dict.keys()) - for pump in self.pumps: - self.add_parameter( - name=pump.lower() + "_state", - label=self.pump_label_dict[pump] + " state", - get_cmd=f"READ:DEV:{pump}:PUMP:SIG:STATE", - get_parser=partial(self._get_parser_state, "STATE"), - set_cmd=partial(self._set_pump_state, pump), - val_mapping={"on": "ON", "off": "OFF"}, - ) - - def _set_pump_state(self, pump: str, state: str) -> None: - self.write(f"SET:DEV:{pump}:PUMP:SIG:STATE:{state}") - - def _get_parser_pump_speed(self, msg: str) -> float | None: - if "NOT_FOUND" in msg: - return None - return float(msg.split("SPD:")[-1].strip("Hz")) - - def _add_temp_state(self) -> None: - for i in range(1, 17): - chan = f"T{i}" - self.add_parameter( - name=chan + "_state", - label=f"Temperature ch{i} state", - get_cmd=f"READ:DEV:{chan}:TEMP:MEAS:ENAB", - get_parser=partial(self._get_parser_state, "ENAB"), - set_cmd=partial(self._set_temp_state, chan), - val_mapping={"on": "ON", "off": "OFF"}, - ) - - def _set_temp_state(self, chan: str, state: str) -> None: - self.write(f"SET:DEV:{chan}:TEMP:MEAS:ENAB:{state}") - - def _get_parser_state(self, key: str, msg: str) -> str | None: - if "NOT_FOUND" in msg: - return None - return msg.split(f"{key}:")[-1] - - -Triton = OxfordTriton -"""Alias for backwards compatibility""" \ No newline at end of file diff --git a/labcore/instruments/qcodes_drivers/SignalCore/SignalCore_sc5511a.py b/labcore/instruments/qcodes_drivers/SignalCore/SignalCore_sc5511a.py deleted file mode 100644 index 35738ea..0000000 --- a/labcore/instruments/qcodes_drivers/SignalCore/SignalCore_sc5511a.py +++ /dev/null @@ -1,981 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Fri Mar 19 14:11:31 2021 - -@author: Chao Zhou - -A simple driver for SignalCore SC5511A to be used with QCoDes, transferred from the one written by Erick Brindock -""" - -import ctypes -import logging -import platform -from typing import Any, Dict, Optional, List - -from qcodes import (Instrument, validators as vals) -from qcodes.utils.validators import Numbers - - -class Device_rf_params_t(ctypes.Structure): - _fields_ = [("rf1_freq", ctypes.c_ulonglong), - ("start_freq", ctypes.c_ulonglong), - ("stop_freq", ctypes.c_ulonglong), - ("step_freq", ctypes.c_ulonglong), - ("sweep_dwell_time", ctypes.c_uint), - ("sweep_cycles", ctypes.c_uint), - ("buffer_time", ctypes.c_uint), - ("rf_level", ctypes.c_float), - ("rf2_freq", ctypes.c_short) - ] - - -class Device_temperature_t(ctypes.Structure): - _fields_ = [("device_temp", ctypes.c_float)] - - -class Operate_status_t(ctypes.Structure): - _fields_ = [("rf1_lock_mode", ctypes.c_ubyte), - ("rf1_loop_gain", ctypes.c_ubyte), - ("device_access", ctypes.c_ubyte), - ("rf2_standby", ctypes.c_ubyte), - ("rf1_standby", ctypes.c_ubyte), - ("auto_pwr_disable", ctypes.c_ubyte), - ("alc_mode", ctypes.c_ubyte), - ("rf1_out_enable", ctypes.c_ubyte), - ("ext_ref_lock_enable", ctypes.c_ubyte), - ("ext_ref_detect", ctypes.c_ubyte), - ("ref_out_select", ctypes.c_ubyte), - ("list_mode_running", ctypes.c_ubyte), - ("rf1_mode", ctypes.c_ubyte), - ("harmonic_ss", ctypes.c_ubyte), - ("over_temp", ctypes.c_ubyte) - ] - - -class Pll_status_t(ctypes.Structure): - _fields_ = [("sum_pll_ld", ctypes.c_ubyte), - ("crs_pll_ld", ctypes.c_ubyte), - ("fine_pll_ld", ctypes.c_ubyte), - ("crs_ref_pll_ld", ctypes.c_ubyte), - ("crs_aux_pll_ld", ctypes.c_ubyte), - ("ref_100_pll_ld", ctypes.c_ubyte), - ("ref_10_pll_ld", ctypes.c_ubyte), - ("rf2_pll_ld", ctypes.c_ubyte)] - - -class List_mode_t(ctypes.Structure): - _fields_ = [("sss_mode", ctypes.c_ubyte), - ("sweep_dir", ctypes.c_ubyte), - ("tri_waveform", ctypes.c_ubyte), - ("hw_trigger", ctypes.c_ubyte), - ("step_on_hw_trig", ctypes.c_ubyte), - ("return_to_start", ctypes.c_ubyte), - ("trig_out_enable", ctypes.c_ubyte), - ("trig_out_on_cycle", ctypes.c_ubyte)] - - -class Device_status_t(ctypes.Structure): - _fields_ = [("list_mode", List_mode_t), - ("operate_status_t", Operate_status_t), - ("pll_status_t", Pll_status_t)] - - -class Device_info_t(ctypes.Structure): - _fields_ = [("serial_number", ctypes.c_uint32), - ("hardware_revision", ctypes.c_float), - ("firmware_revision", ctypes.c_float), - ("manufacture_date", ctypes.c_uint32) - ] - - -# End of Structures------------------------------------------------------------ -class SignalCore_SC5511A(Instrument): - - if platform.system() == 'Windows': - dllpath = r"C:\Program Files\SignalCore\SC5511A\api\c\x64\sc5511a.dll" - else: - dllpath = r"/home/pfafflab/Documents/drivers/Linux/libusb/lib/libsc55511a.so.1.0" - - def __init__(self, name: str, serial_number: str, - dllpath: Optional[str] = None, debug=False, **kwargs: Any): - super().__init__(name, **kwargs) - - logging.info(__name__ + f' : Initializing instrument SignalCore generator {serial_number}') - if dllpath is not None: - self._dll = ctypes.CDLL(dllpath) - else: - self._dll = ctypes.CDLL(self.dllpath) - - if debug: - print(self._dll) - - self._dll.sc5511a_open_device.restype = ctypes.c_uint64 - self._handle = ctypes.c_void_p( - self._dll.sc5511a_open_device(ctypes.c_char_p(bytes(serial_number, 'utf-8')))) - self._serial_number = ctypes.c_char_p(bytes(serial_number, 'utf-8')) - self._rf_params = Device_rf_params_t(0, 0, 0, 0, 0, 0, 0, 0, 0) - self._status = Operate_status_t(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) - self._open = False - self._temperature = Device_temperature_t(0) - - self._pll_status = Pll_status_t() - self._list_mode = List_mode_t() - self._device_status = Device_status_t(self._list_mode, self._status, self._pll_status) - if debug: - print(serial_number, self._handle) - self._dll.sc5511a_get_device_status(self._handle, ctypes.byref(self._device_status)) - status = self._device_status.operate_status_t.rf1_out_enable - print('check status', status) - - self._dll.sc5511a_close_device(self._handle) - self._device_info = Device_info_t(0, 0, 0, 0) - self.get_idn() - self.do_set_auto_level_disable(0) # setting this to 1 will lead to unstable output power - - self.add_parameter('sweep_start_frequency', - label='sweep_start_frequency', - get_cmd=self.do_get_sweep_start_frequency, - get_parser=float, - set_cmd=self.do_set_sweep_start_frequency, - set_parser=float, - unit='Hz', - vals=Numbers(min_value=0, max_value=20e9) - ) - - self.add_parameter('sweep_stop_frequency', - label='sweep_stop_frequency', - get_cmd=self.do_get_sweep_stop_frequency, - get_parser=float, - set_cmd=self.do_set_sweep_stop_frequency, - set_parser=float, - unit='Hz', - vals=Numbers(min_value=0, max_value=20e9) - ) - - self.add_parameter('sweep_step_frequency', - label='sweep_step_frequency', - get_cmd=self.do_get_sweep_step_frequency, - get_parser=float, - set_cmd=self.do_set_sweep_step_frequency, - set_parser=float, - unit='Hz', - vals=Numbers(min_value=0, max_value=20e9) - ) - - self.add_parameter('sweep_dwell_time', - label='sweep_dwell_time', - get_cmd=self.do_get_sweep_dwell_time, - get_parser=int, - set_cmd=self.do_set_sweep_dwell_time, - set_parser=int, - unit='', - vals=Numbers(min_value=1) - ) - - self.add_parameter('sweep_cycles', - label='sweep_cycles', - get_cmd=self.do_get_sweep_cycles, - get_parser=int, - set_cmd=self.do_set_sweep_cycles, - set_parser=int, - unit='', - vals=Numbers(min_value=0) - ) - - self.add_parameter('trig_out_enable', - label='trig_out_enable', - get_cmd=self.do_get_trig_out_enable, - set_cmd=self.do_set_trig_out_enable, - unit='', - vals=vals.Enum(0, 1) - ) - - self.add_parameter('trig_out_on_cycle', - label='trig_out_on_cycle', - get_cmd=self.do_get_trig_out_on_cycle, - set_cmd=self.do_set_trig_out_on_cycle, - unit='', - vals=vals.Enum(0, 1) - ) - - self.add_parameter('step_on_hw_trig', - label='step_on_hw_trig', - get_cmd=self.do_get_step_on_hw_trig, - set_cmd=self.do_set_step_on_hw_trig, - unit='', - vals=vals.Enum(0, 1) - ) - - self.add_parameter('return_to_start', - label='return_to_start', - get_cmd=self.do_get_return_to_start, - set_cmd=self.do_set_return_to_start, - unit='', - vals=vals.Enum(0, 1) - ) - - self.add_parameter('hw_trigger', - label='hw_trigger', - get_cmd=self.do_get_hw_trig, - set_cmd=self.do_set_hw_trig, - unit='', - vals=vals.Enum(0, 1) - ) - - self.add_parameter('tri_waveform', - label='tri_waveform', - get_cmd=self.do_get_tri_waveform, - set_cmd=self.do_set_tri_waveform, - unit='', - vals=vals.Enum(0, 1) - ) - - self.add_parameter('sweep_dir', - label='sweep_dir', - get_cmd=self.do_get_sweep_dir, - set_cmd=self.do_set_sweep_dir, - unit='', - vals=vals.Enum(0, 1) - ) - - self.add_parameter('sss_mode', - label='sss_mode', - get_cmd=self.do_get_sss_mode, - set_cmd=self.do_set_sss_mode, - unit='', - vals=vals.Enum(0, 1) - ) - - self.add_parameter('rf1_mode', - label='rf1_mode', - get_cmd=self.do_get_rf1_mode, - set_cmd=self.do_set_rf1_mode, - unit='', - ) - - self.add_parameter('power', - label='power', - get_cmd=self.do_get_power, - get_parser=float, - set_cmd=self.do_set_power, - set_parser=float, - unit='dBm', - vals=Numbers(min_value=-144, max_value=19)) - - self.add_parameter('output_status', - label='output_status', - get_cmd=self.do_get_output_status, - get_parser=int, - set_cmd=self.do_set_output_status, - set_parser=int, - vals=Numbers(min_value=0, max_value=1)) - - self.add_parameter('frequency', - label='frequency', - get_cmd=self.do_get_frequency, - get_parser=float, - set_cmd=self.do_set_frequency, - set_parser=float, - unit='Hz', - vals=Numbers(min_value=0, max_value=20e9)) - - self.add_parameter('reference_source', - label='reference_source', - get_cmd=self.do_get_reference_source, - get_parser=int, - set_cmd=self.do_set_reference_source, - set_parser=int, - vals=Numbers(min_value=0, max_value=1)) - - self.add_parameter('auto_level_disable', - label='0 = power is leveled on frequency change', - get_cmd=self.do_get_auto_level_disable, - get_parser=int, - set_cmd=self.do_set_auto_level_disable, - set_parser=int, - vals=Numbers(min_value=0, max_value=1)) - - self.add_parameter('temperature', - label='temperature', - get_cmd=self.do_get_device_temp, - get_parser=float, - unit="C", - vals=Numbers(min_value=0, max_value=200)) - - if self._device_status.operate_status_t.ext_ref_lock_enable == 0: - self.do_set_reference_source(1) - - @classmethod - def connected_instruments(cls, max_n_gens: int = 100, sn_len: int = 100) -> List[str]: - """ - Return the serial numbers of the connected generators. - - The parameters are very unlikely to be needed, and are just for making sure - we allocated the right amount of memory when calling the SignalCore DLL. - Parameters: - max_n_gens: maximum number of generators expected - sn_len: max length of serial numbers. - """ - dll = ctypes.CDLL(cls.dllpath) - search = dll.sc5511a_search_devices - - # generate and allocate string memory - mem_type = (ctypes.c_char_p * max_n_gens) - mem = mem_type() - for i in range(max_n_gens): - mem[i] = b' ' * sn_len - - search.argtypes = [mem_type] - n_gens_found = search(mem) - return [sn.decode('utf-8') for sn in mem[:n_gens_found]] - - def set_open(self, open) -> bool: - if open and not self._open: - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - self._open = True - elif not open and self._open: - self._dll.sc5511a_close_device(self._handle) - self._open = False - return True - - def soft_trigger(self) -> None: - """ - Send out a soft trigger, so that the we can start the sweep - Generator need to be configured for list mode and soft trigger is selected as the trigger source - """ - logging.info(__name__ + ' : Send a soft trigger to the generator') - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - self._dll.sc5511a_list_soft_trigger(self._handle) - self._dll.sc5511a_close_device(self._handle) - return None - - def do_set_output_status(self, enable) -> None: - """ - Turns the output of RF1 on or off. - Input: - enable (int) = OFF = 0 ; ON = 1 - """ - logging.info(__name__ + ' : Setting output to %s' % enable) - c_enable = ctypes.c_ubyte(enable) - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - completed = self._dll.sc5511a_set_output(self._handle, c_enable) - self._dll.sc5511a_close_device(self._handle) - return completed - - def do_get_output_status(self) -> int: - """ - Reads the output status of RF1 - Output: - status (int) : OFF = 0 ; ON = 1 - """ - logging.info(__name__ + ' : Getting output') - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - self._dll.sc5511a_get_device_status(self._handle, ctypes.byref(self._device_status)) - status = self._device_status.operate_status_t.rf1_out_enable - self._dll.sc5511a_close_device(self._handle) - return status - - def do_set_sweep_start_frequency(self, sweep_start_frequency) -> None: - """ - Set the sweep start frequency of RF1 in the unit of Hz - """ - c_sweep_start_freq = ctypes.c_ulonglong(int(sweep_start_frequency)) - logging.info(__name__ + ' : Setting sweep start frequency to %s' % sweep_start_frequency) - close = False - if not self._open: - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - close = True - if_set = self._dll.sc5511a_list_start_freq(self._handle, c_sweep_start_freq) - if close: - self._dll.sc5511a_close_device(self._handle) - return if_set - - def do_get_sweep_start_frequency(self) -> float: - """ - Get the sweep start frequency that is used in the sweep mode - The frequency returned is in the unit of Hz - """ - logging.info(__name__ + 'Getting sweep start frequency') - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - self._dll.sc5511a_get_rf_parameters(self._handle, ctypes.byref(self._rf_params)) - sweep_start_frequency = self._rf_params.start_freq - self._dll.sc5511a_close_device(self._handle) - return sweep_start_frequency - - def do_set_sweep_stop_frequency(self, sweep_stop_frequency) -> None: - """ - Set the sweep stop frequency of RF1 in the unit of Hz - """ - c_sweep_stop_frequency = ctypes.c_ulonglong(int(sweep_stop_frequency)) - logging.info(__name__ + ' : Setting sweep stop frequency to %s' % sweep_stop_frequency) - close = False - if not self._open: - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - close = True - if_set = self._dll.sc5511a_list_stop_freq(self._handle, c_sweep_stop_frequency) - if close: - self._dll.sc5511a_close_device(self._handle) - return if_set - - def do_get_sweep_stop_frequency(self) -> float: - """ - Get the sweep stop frequency that is used in the sweep mode - The frequency returned is in the unit of Hz - """ - logging.info(__name__ + 'Getting sweep stop frequency') - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - self._dll.sc5511a_get_rf_parameters(self._handle, ctypes.byref(self._rf_params)) - sweep_stop_frequency = self._rf_params.stop_freq - self._dll.sc5511a_close_device(self._handle) - return sweep_stop_frequency - - def do_set_sweep_step_frequency(self, sweep_step_frequency) -> None: - """ - Set the sweep step frequency of RF1 in the unit of Hz - """ - c_sweep_step_frequency = ctypes.c_ulonglong(int(sweep_step_frequency)) - logging.info(__name__ + ' : Setting sweep step frequency to %s' % sweep_step_frequency) - close = False - if not self._open: - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - close = True - if_set = self._dll.sc5511a_list_step_freq(self._handle, c_sweep_step_frequency) - if close: - self._dll.sc5511a_close_device(self._handle) - return if_set - - def do_get_sweep_step_frequency(self) -> float: - """ - Get the sweep step frequency that is used in the sweep mode - The frequency returned is in the unit of Hz - """ - logging.info(__name__ + 'Getting sweep step frequency') - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - self._dll.sc5511a_get_rf_parameters(self._handle, ctypes.byref(self._rf_params)) - sweep_step_frequency = self._rf_params.step_freq - self._dll.sc5511a_close_device(self._handle) - return sweep_step_frequency - - def do_set_sweep_dwell_time(self, sweep_dwell_time) -> None: - """ - Set the sweep/list time at each frequency point. - Note that the dwell time is set as multiple of 500 us. - The input value is an unsigned int, it means how many multiple of 500 us. - """ - c_sweep_dwell_time = ctypes.c_uint(int(sweep_dwell_time)) - logging.info(__name__ + ': Setting sweep dwell time to %s' % sweep_dwell_time) - close = False - if not self._open: - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - close = True - if_set = self._dll.sc5511a_list_dwell_time(self._handle, c_sweep_dwell_time) - if close: - self._dll.sc5511a_close_device(self._handle) - return if_set - - def do_get_sweep_dwell_time(self) -> int: - """ - Get the dwell time of the sweep mode. - Return value is the unit multiple of 500 us, e.g. a return value 3 means the dwell time is 1500 us. - """ - logging.info(__name__ + 'Getting sweep dwell time in the unit of how many multiple of 500 us') - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - self._dll.sc5511a_get_rf_parameters(self._handle, ctypes.byref(self._rf_params)) - sweep_dwell_time = self._rf_params.sweep_dwell_time - self._dll.sc5511a_close_device(self._handle) - return sweep_dwell_time - - def do_set_sweep_cycles(self, sweep_cycles) -> None: - """ - Set the number of sweep cycles to perform before stopping. - To repeat the sweep continuously, set the value to 0. - """ - c_sweep_cycles = ctypes.c_uint(int(sweep_cycles)) - logging.info(__name__ + ': Setting sweep cycle number to %s ' % sweep_cycles) - close = False - if not self._open: - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - close = True - if_set = self._dll.sc5511a_list_cycle_count(self._handle, c_sweep_cycles) - if close: - self._dll.sc5511a_close_device(self._handle) - return if_set - - def do_get_sweep_cycles(self) -> int: - """ - Get the number of sweep cycles to perform before stopping. - To repeat the sweep continuously, the value is 0. - """ - logging.info(__name__ + 'Getting number of sweep cycles') - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - self._dll.sc5511a_get_rf_parameters(self._handle, ctypes.byref(self._rf_params)) - sweep_cycles = self._rf_params.sweep_cycles - self._dll.sc5511a_close_device(self._handle) - return sweep_cycles - - def do_set_trig_out_enable(self, trig_out_enable) -> None: - """ - Set the trigger output status. - It does not send out the trigger, just enable the generator to send out the trigger - 0 = No trigger output - 1 = Puts a trigger pulse on the TRIGOUT pin - """ - c_trig_out_enable = ctypes.c_ubyte(int(trig_out_enable)) - logging.info(__name__ + ': Setting sweep cycle number to %s ' % trig_out_enable) - close = False - if not self._open: - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - close = True - - self._dll.sc5511a_get_device_status(self._handle, ctypes.byref(self._device_status)) - self._device_status.list_mode.trig_out_enable = c_trig_out_enable - if_set = self._dll.sc5511a_list_mode_config(self._handle, ctypes.byref(self._device_status.list_mode)) - - if close: - self._dll.sc5511a_close_device(self._handle) - return if_set - - def do_get_trig_out_enable(self) -> int: - """ - Get the status of the trigger output status - 0 = No trigger output - 1 = Puts a trigger pulse on the TRIGOUT pin - """ - logging.info(__name__ + 'Getting trigger output status') - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - self._dll.sc5511a_get_device_status(self._handle, ctypes.byref(self._device_status)) - trig_out_enable = self._device_status.list_mode.trig_out_enable - self._dll.sc5511a_close_device(self._handle) - return trig_out_enable - - def do_set_trig_out_on_cycle(self, trig_out_on_cycle) -> None: - """ - Set the trigger output mode - 0 = Puts out a trigger pulse at each frequency change - 1 = Puts out a trigger pulse at the completion of each sweep/list cycle - """ - c_trig_out_on_cycle = ctypes.c_ubyte(int(trig_out_on_cycle)) - logging.info(__name__ + ': Setting sweep cycle number to %s ' % trig_out_on_cycle) - close = False - if not self._open: - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - close = True - - self._dll.sc5511a_get_device_status(self._handle, ctypes.byref(self._device_status)) - self._device_status.list_mode.trig_out_on_cycle = c_trig_out_on_cycle - if_set = self._dll.sc5511a_list_mode_config(self._handle, ctypes.byref(self._device_status.list_mode)) - - if close: - self._dll.sc5511a_close_device(self._handle) - return if_set - - def do_get_trig_out_on_cycle(self) -> int: - """ - Get the trigger output mode - 0 = Puts out a trigger pulse at each frequency change - 1 = Puts out a trigger pulse at the completion of each sweep/list cycle - """ - logging.info(__name__ + 'Getting trigger output mode ') - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - self._dll.sc5511a_get_device_status(self._handle, ctypes.byref(self._device_status)) - trig_out_enable = self._device_status.list_mode.trig_out_on_cycle - self._dll.sc5511a_close_device(self._handle) - return trig_out_enable - - def do_set_step_on_hw_trig(self, step_on_hw_trig) -> None: - """ - Set the behavior of the sweep/list mode when receiving a trigger. - 0 = Start/Stop behavior. The sweep starts and continues to step through the list for the number of cycles set, - dwelling at each step frequency for a period set by the dwell time. The sweep/list will end on a consecutive - trigger. - 1 = Step-on-trigger. This is only available if hardware triggering is selected. The device will step to the next - frequency on a trigger.Upon completion of the number of cycles, the device will exit from the stepping state - and stop. - """ - c_step_on_hw_trig = ctypes.c_ubyte(int(step_on_hw_trig)) - logging.info(__name__ + ': Setting sweep cycle number to %s ' % step_on_hw_trig) - close = False - if not self._open: - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - close = True - - self._dll.sc5511a_get_device_status(self._handle, ctypes.byref(self._device_status)) - self._device_status.list_mode.step_on_hw_trig = c_step_on_hw_trig - if_set = self._dll.sc5511a_list_mode_config(self._handle, ctypes.byref(self._device_status.list_mode)) - - if close: - self._dll.sc5511a_close_device(self._handle) - return if_set - - def do_get_step_on_hw_trig(self) -> int: - """ - Set the behavior of the sweep/list mode when receiving a trigger. - 0 = Start/Stop behavior - 1 = Step-on-trigger - """ - logging.info(__name__ + 'Getting status of step on trigger mode ') - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - self._dll.sc5511a_get_device_status(self._handle, ctypes.byref(self._device_status)) - step_on_hw_trig = self._device_status.list_mode.step_on_hw_trig - self._dll.sc5511a_close_device(self._handle) - return step_on_hw_trig - - def do_set_return_to_start(self, return_to_start) -> None: - """ - Set how the frequency will change at the end of the list/sweep - 0 = Stop at end of sweep/list. The frequency will stop at the last point of the sweep/list - 1 = Return to start. The frequency will return and stop at the beginning point of the sweep or list after a - cycle. - """ - c_return_to_start = ctypes.c_ubyte(int(return_to_start)) - logging.info(__name__ + ': Setting sweep cycle number to %s ' % return_to_start) - close = False - if not self._open: - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - close = True - - self._dll.sc5511a_get_device_status(self._handle, ctypes.byref(self._device_status)) - self._device_status.list_mode.return_to_start = c_return_to_start - if_set = self._dll.sc5511a_list_mode_config(self._handle, ctypes.byref(self._device_status.list_mode)) - - if close: - self._dll.sc5511a_close_device(self._handle) - return if_set - - def do_get_return_to_start(self) -> int: - """ - Get the status of how the frequency will change at the end of the list/sweep - 0 = Stop at end of sweep/list. The frequency will stop at the last point of the sweep/list - 1 = Return to start. The frequency will return and stop at the beginning point of the sweep or list after a - cycle. - """ - logging.info(__name__ + 'Getting status of return to start ') - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - self._dll.sc5511a_get_device_status(self._handle, ctypes.byref(self._device_status)) - return_to_start = self._device_status.list_mode.return_to_start - self._dll.sc5511a_close_device(self._handle) - return return_to_start - - def do_set_hw_trig(self, hw_trigger) -> None: - """ - Set the status of hardware trigger - 0 = Software trigger. Softtrigger can only be used to start and stop a sweep/list cycle. It does not work for - step-on-trigger mode. - 1 = Hardware trigger. A high-to-low transition on the TRIGIN pin will trigger the device. It can be used for - both start/stop or step-on-trigger functions. - """ - c_hw_trigger = ctypes.c_ubyte(int(hw_trigger)) - logging.info(__name__ + ': Setting sweep cycle number to %s ' % hw_trigger) - close = False - if not self._open: - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - close = True - - self._dll.sc5511a_get_device_status(self._handle, ctypes.byref(self._device_status)) - self._device_status.list_mode.hw_trigger = c_hw_trigger - if_set = self._dll.sc5511a_list_mode_config(self._handle, ctypes.byref(self._device_status.list_mode)) - - if close: - self._dll.sc5511a_close_device(self._handle) - return if_set - - def do_get_hw_trig(self) -> int: - """ - Get the status of hardware trigger - 0 = Software trigger. Softtrigger can only be used to start and stop a sweep/list cycle. It does not work for - step-on-trigger mode. - 1 = Hardware trigger. A high-to-low transition on the TRIGIN pin will trigger the device. It can be used for - both start/stop or step-on-trigger functions. - """ - logging.info(__name__ + 'Getting status of hardware trigger ') - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - self._dll.sc5511a_get_device_status(self._handle, ctypes.byref(self._device_status)) - hw_trigger = self._device_status.list_mode.hw_trigger - self._dll.sc5511a_close_device(self._handle) - return hw_trigger - - def do_set_tri_waveform(self, tri_waveform) -> None: - """ - Set the triangular waveform of the generator - 0 = Sawtooth waveform. Frequency returns to the beginning frequency upon reaching the end of a sweep cycle - 1 = Triangular waveform. Frequency reverses direction at the end of the list and steps back towards the - beginning to complete a cycle - """ - c_tri_waveform = ctypes.c_ubyte(int(tri_waveform)) - logging.info(__name__ + ': Setting sweep cycle number to %s ' % tri_waveform) - close = False - if not self._open: - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - close = True - - self._dll.sc5511a_get_device_status(self._handle, ctypes.byref(self._device_status)) - self._device_status.list_mode.tri_waveform = c_tri_waveform - if_set = self._dll.sc5511a_list_mode_config(self._handle, ctypes.byref(self._device_status.list_mode)) - - if close: - self._dll.sc5511a_close_device(self._handle) - return if_set - - def do_get_tri_waveform(self) -> int: - """ - Get the triangular waveform of the generator - 0 = Sawtooth waveform. Frequency returns to the beginning frequency upon reaching the end of a sweep cycle - 1 = Triangular waveform. Frequency reverses direction at the end of the list and steps back towards the - beginning to complete a cycle - """ - logging.info(__name__ + 'Getting status of triangular waveform ') - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - self._dll.sc5511a_get_device_status(self._handle, ctypes.byref(self._device_status)) - hw_trigger = self._device_status.list_mode.tri_waveform - self._dll.sc5511a_close_device(self._handle) - return hw_trigger - - def do_set_sweep_dir(self, sweep_dir) -> None: - """ - Set the sweep direction of the generator - 0 = Forward. Sweeps start from the lowest start frequency or starts at the beginning of the list buffer - 1 = Reverse. Sweeps start from the stop frequency and steps down toward the start frequency or starts at the - end and steps toward the beginning of the buffer - """ - c_sweep_dir = ctypes.c_ubyte(int(sweep_dir)) - logging.info(__name__ + ': Setting sweep cycle number to %s ' % sweep_dir) - close = False - if not self._open: - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - close = True - - self._dll.sc5511a_get_device_status(self._handle, ctypes.byref(self._device_status)) - self._device_status.list_mode.sweep_dir = c_sweep_dir - if_set = self._dll.sc5511a_list_mode_config(self._handle, ctypes.byref(self._device_status.list_mode)) - - if close: - self._dll.sc5511a_close_device(self._handle) - return if_set - - def do_get_sweep_dir(self) -> int: - """ - Get the sweep direction of the generator - 0 = Forward. Sweeps start from the lowest start frequency or starts at the beginning of the list buffer - 1 = Reverse. Sweeps start from the stop frequency and steps down toward the start frequency or starts at the - end and steps toward the beginning of the buffer - """ - logging.info(__name__ + 'Getting status of sweep direction ') - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - self._dll.sc5511a_get_device_status(self._handle, ctypes.byref(self._device_status)) - hw_trigger = self._device_status.list_mode.sweep_dir - self._dll.sc5511a_close_device(self._handle) - return hw_trigger - - def do_set_sss_mode(self, sss_mode) -> None: - """ - Set the list/sweep mode of the generator - 0 = List mode. Device gets its frequency points from the list buffer uploaded via LIST_BUFFER_WRITE register - 1 = Sweep mode. The device computes the frequency points using the Start, Stop and Step frequencies - """ - c_sss_mode = ctypes.c_ubyte(int(sss_mode)) - logging.info(__name__ + ': Setting sweep cycle number to %s ' % sss_mode) - close = False - if not self._open: - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - close = True - - self._dll.sc5511a_get_device_status(self._handle, ctypes.byref(self._device_status)) - self._device_status.list_mode.sss_mode = c_sss_mode - if_set = self._dll.sc5511a_list_mode_config(self._handle, ctypes.byref(self._device_status.list_mode)) - - if close: - self._dll.sc5511a_close_device(self._handle) - return if_set - - def do_get_sss_mode(self) -> int: - """ - Get the list/sweep mode of the generator - 0 = List mode. Device gets its frequency points from the list buffer uploaded via LIST_BUFFER_WRITE register - 1 = Sweep mode. The device computes the frequency points using the Start, Stop and Step frequencies - """ - logging.info(__name__ + 'Getting status of sss mode') - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - self._dll.sc5511a_get_device_status(self._handle, ctypes.byref(self._device_status)) - sss_mode = self._device_status.list_mode.sss_mode - self._dll.sc5511a_close_device(self._handle) - return sss_mode - - def do_set_rf1_mode(self, rf1_mode) -> None: - """ - Set the RF mode for rf1 - 0 = single fixed tone mode - 1 = sweep/list mode - """ - c_rf1_mode = ctypes.c_ubyte(rf1_mode) - logging.info(__name__ + ' : Setting frequency to %s' % rf1_mode) - close = False - if not self._open: - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - close = True - if_set = self._dll.sc5511a_set_rf_mode(self._handle, c_rf1_mode) - if close: - self._dll.sc5511a_close_device(self._handle) - return if_set - - def do_get_rf1_mode(self) -> int: - """ - Get the RF mode for rf1 - 0 = single fixed tone mode - 1 = sweep/list mode - """ - logging.info(__name__ + 'Getting the RF mode for rf1') - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - self._dll.sc5511a_get_device_status(self._handle, ctypes.byref(self._device_status)) - rf1_mode = self._device_status.operate_status_t.rf1_mode - self._dll.sc5511a_close_device(self._handle) - return rf1_mode - - def do_set_frequency(self, frequency) -> None: - """ - Sets RF1 frequency in the unit of Hz. Valid between 100MHz and 20GHz - Args: - frequency (int) = frequency in Hz - """ - c_freq = ctypes.c_ulonglong(int(frequency)) - logging.info(__name__ + ' : Setting frequency to %s' % frequency) - close = False - if not self._open: - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - close = True - if_set = self._dll.sc5511a_set_freq(self._handle, c_freq) - if close: - self._dll.sc5511a_close_device(self._handle) - return if_set - - def do_get_frequency(self) -> float: - """ - Gets RF1 frequency in the unit of Hz. - """ - logging.info(__name__ + ' : Getting frequency') - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - self._dll.sc5511a_get_rf_parameters(self._handle, ctypes.byref(self._rf_params)) - frequency = self._rf_params.rf1_freq - self._dll.sc5511a_close_device(self._handle) - return frequency - - def do_set_reference_source(self, lock_to_external) -> None: - """ - Set the generator reference source - 0 = internal source - 1 = external source - - Note here high is set to 0, means we always use 10 MHz clock when use external lock - """ - logging.info(__name__ + ' : Setting reference source to %s' % lock_to_external) - high = ctypes.c_ubyte(0) - lock = ctypes.c_ubyte(lock_to_external) - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - source = self._dll.sc5511a_set_clock_reference(self._handle, high, lock) - self._dll.sc5511a_close_device(self._handle) - return source - - def do_get_reference_source(self) -> int: - """ - Get the generator reference source - 0 = internal source - 1 = external source - """ - logging.info(__name__ + ' : Getting reference source') - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - enabled = self._device_status.operate_status_t.ext_ref_lock_enable - self._dll.sc5511a_close_device(self._handle) - return enabled - - def do_set_power(self, power) -> None: - """ - Set the power of the generator in the unit of dBm - """ - logging.info(__name__ + ' : Setting power to %s' % power) - c_power = ctypes.c_float(power) - close = False - if not self._open: - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - close = True - completed = self._dll.sc5511a_set_level(self._handle, c_power) - if close: - self._dll.sc5511a_close_device(self._handle) - return completed - - def do_get_power(self) -> float: - """ - Get the power of the generator in the unit of dBm - """ - logging.info(__name__ + ' : Getting Power') - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - self._dll.sc5511a_get_rf_parameters(self._handle, ctypes.byref(self._rf_params)) - rf_level = self._rf_params.rf_level - self._dll.sc5511a_close_device(self._handle) - return rf_level - - def do_set_auto_level_disable(self, enable) -> None: - """ - Set if we want to disable the auto level - """ - logging.info(__name__ + ' : Settingalc auto to %s' % enable) - if enable == 1: - enable = 0 - elif enable == 0: - enable = 1 - c_enable = ctypes.c_ubyte(enable) - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - completed = self._dll.sc5511a_set_auto_level_disable(self._handle, c_enable) - self._dll.sc5511a_close_device(self._handle) - return completed - - def do_get_auto_level_disable(self) -> int: - """ - Get if we disable to auto level - """ - logging.info(__name__ + ' : Getting alc auto status') - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - self._dll.sc5511a_get_device_status(self._handle, ctypes.byref(self._device_status)) - enabled = self._device_status.operate_status_t.auto_pwr_disable - self._dll.sc5511a_close_device(self._handle) - if enabled == 1: - enabled = 0 - elif enabled == 0: - enabled = 1 - return enabled - - def do_get_device_temp(self) -> float: - """ - Get the device temperature in unit of C - """ - logging.info(__name__ + " : Getting device temperature") - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - self._dll.sc5511a_get_temperature(self._handle, ctypes.byref(self._temperature)) - device_temp = self._temperature.device_temp - self._dll.sc5511a_close_device(self._handle) - return device_temp - - def get_idn(self) -> Dict[str, Optional[str]]: - """ - Get the identification information of the current device - """ - logging.info(__name__ + " : Getting device info") - self._handle = ctypes.c_void_p(self._dll.sc5511a_open_device(self._serial_number)) - self._dll.sc5511a_get_device_info(self._handle, ctypes.byref(self._device_info)) - device_info = self._device_info - self._dll.sc5511a_close_device(self._handle) - - def date_decode(date_int: int): - date_str = f"{date_int:032b}" - yr = f"20{int(date_str[:8], 2)}" - month = f"{int(date_str[16:24], 2)}" - day = f"{int(date_str[8:16], 2)}" - return f"{month}/{day}/{yr}" - - IDN: Dict[str, Optional[str]] = { - 'vendor': "SignalCore", - 'model': "SC5511A", - 'serial_number': self._serial_number.value.decode("utf-8"), - 'firmware_revision': device_info.firmware_revision, - 'hardware_revision': device_info.hardware_revision, - 'manufacture_date': date_decode(device_info.manufacture_date) - } - return IDN diff --git a/labcore/instruments/qcodes_drivers/SignalCore/SignalCore_sc5521a.py b/labcore/instruments/qcodes_drivers/SignalCore/SignalCore_sc5521a.py deleted file mode 100644 index c9343f3..0000000 --- a/labcore/instruments/qcodes_drivers/SignalCore/SignalCore_sc5521a.py +++ /dev/null @@ -1,783 +0,0 @@ -"Made by jenshnielse. Edited to add more functions by Randy Owen" - -import ctypes #foreign function library. Provide C-compatible data types -import ctypes.wintypes #widnow specific data types -import os #for communicating with the operating system. Managing files+ -import sys #for accessing what the python intepreter is seeing -from typing import Dict, Optional # for talking to earlier versions of python and defines standard notations -from qcodes import Instrument #Intrument class of the qcodes package -from qcodes.utils.validators import Enum,Numbers,Ints,Multiples,PermissiveMultiples -#validators check if an arguement is of a certain type. -#Enum requires that ones of a provided set of values match the arguement - - -MAXDEVICES = 50 # the number of signal cores it looks for, I think -MAXDESCRIPTORSIZE = 9 -COMMINTERFACE = ctypes.c_uint8(1) - -#the next blocks of code are defining Classes of ctype.Structure that are like a dictonary -# for telling C what type of object each keyword is and how to store it -#for example the ctypes.c_int8 is for passing C an unsigned interger byte -# -class ManDate(ctypes.Structure): #defining how to represent the manufacturing date - _fields_ = [('year', ctypes.c_uint8), - ('month', ctypes.c_uint8), - ('day', ctypes.c_uint8), - ('hour', ctypes.c_uint8)] - - -class DeviceInfoT(ctypes.Structure): #defining how to represent the device information - _fields_ = [('product_serial_number', ctypes.c_uint32), #uses 32-bit because serial number is long - ('hardware_revision', ctypes.c_float), #uses float because it expects decimal - ('firmware_revision', ctypes.c_float), - ('device_interfaces', ctypes.c_uint8), - ('man_date', ManDate)] #expects the variables defined in the ManData class -device_info_t = DeviceInfoT() #making an object instance of the DeviceInfoT Class so its attributes can be called later - - -class ListModeT(ctypes.Structure): #defining how to represent the type of sweeping mode - _fields_ = [('sweep_mode', ctypes.c_uint8), - ('sweep_dir', ctypes.c_uint8), - ('tri_waveform', ctypes.c_uint8), - ('hw_trigger', ctypes.c_uint8), - ('step_on_hw_trig', ctypes.c_uint8), - ('return_to_start', ctypes.c_uint8), - ('trig_out_enable', ctypes.c_uint8), - ('trig_out_on_cycle', ctypes.c_uint8)] -list_mode_t=ListModeT() - -class PLLStatusT(ctypes.Structure): #defining how to represent the phase lock loop status - _fields_ = [('sum_pll_ld', ctypes.c_uint8), - ('crs_pll_ld', ctypes.c_uint8), - ('fine_pll_ld', ctypes.c_uint8), - ('crs_ref_pll_ld', ctypes.c_uint8), - ('crs_aux_pll_ld', ctypes.c_uint8), - ('ref_100_pll_ld', ctypes.c_uint8), - ('ref_10_pll_ld', ctypes.c_uint8)] -pll_status_t=PLLStatusT() - -class OperateStatusT(ctypes.Structure): #defining how to represent Operating statues - _fields_ = [('rf1_lock_mode', ctypes.c_uint8), - ('rf1_loop_gain', ctypes.c_uint8), - ('device_access', ctypes.c_uint8), - ('device_standby', ctypes.c_uint8), - ('auto_pwr_disable', ctypes.c_uint8), - ('output_enable', ctypes.c_uint8), - ('ext_ref_lock_enable', ctypes.c_uint8), - ('ext_ref_detect', ctypes.c_uint8), - ('ref_out_select', ctypes.c_uint8), - ('list_mode_running', ctypes.c_uint8), - ('rf_mode', ctypes.c_uint8), - ('over_temp', ctypes.c_uint8), - ('harmonic_ss', ctypes.c_uint8), - ('pci_clk_enable', ctypes.c_uint8)] -operate_status_t=OperateStatusT() - -class DeviceStatusT(ctypes.Structure): #seems to define a higher class that contains all operating mode details - _fields_ = [('list_mode_t', ListModeT), - ('operate_status_t', OperateStatusT), - ('pll_status_t', PLLStatusT)] -device_status_t = DeviceStatusT() - - -class HWTriggerT(ctypes.Structure): - _fields_ = [('edge', ctypes.c_uint8), - ('pxi_enable', ctypes.c_uint8), - ('pxi_line', ctypes.c_uint8)] -hw_trigger_t = HWTriggerT() - - -class DeviceRFParamsT(ctypes.Structure): #defining the RF parameters of the sweeps - _fields_ = [('frequency', ctypes.c_double), - ('sweep_start_freq', ctypes.c_double), - ('sweep_stop_freq', ctypes.c_double), - ('sweep_step_freq', ctypes.c_double), - ('sweep_dwell_time', ctypes.c_uint32), - ('sweep_cycles', ctypes.c_uint32), - ('buffer_points', ctypes.c_uint32), - ('rf_phase_offset', ctypes.c_float), - ('power_level', ctypes.c_float), - ('atten_value', ctypes.c_float), - ('level_dac_value', ctypes.c_uint16)] -device_rf_params_t = DeviceRFParamsT() - -#the dictonary of errors -error_dict = {'0':'SCI_SUCCESS', - '0':'SCI_ERROR_NONE', - '-1':'SCI_ERROR_INVALID_DEVICE_HANDLE', - '-2':'SCI_ERROR_NO_DEVICE', - '-3':'SCI_ERROR_INVALID_DEVICE', - '-4':'SCI_ERROR_MEM_UNALLOCATE', - '-5':'SCI_ERROR_MEM_EXCEEDED', - '-6':'SCI_ERROR_INVALID_REG', - '-7':'SCI_ERROR_INVALID_ARGUMENT', - '-8':'SCI_ERROR_COMM_FAIL', - '-9':'SCI_ERROR_OUT_OF_RANGE', - '-10':'SCI_ERROR_PLL_LOCK', - '-11':'SCI_ERROR_TIMED_OUT', - '-12':'SCI_ERROR_COMM_INIT', - '-13':'SCI_ERROR_TIMED_OUT_READ', - '-14':'SCI_ERROR_INVALID_INTERFACE'} - - -#defining the qcodes insturment class for the device -class SC5521A(Instrument): - __doc__ = 'QCoDeS python driver for the Signal Core SC5521A.' - - def __init__(self, name: str, #name the intsrument - serial_number:str, - dll_path: str='SignalCore\\SC5520A\\api\\c\\scipci\\x64\\sc5520a_uhfs.dll', - #a path to the DLL of the device, which is the C program that actually drives the device - #note, that this works for SC5521A despite the name - **kwargs): - """ - QCoDeS driver for the Signal Core SC5521A. - This driver has been tested when only one SignalCore is connected to the - computer. - - Args: - name (str): Name of the instrument. - dll_path (str): Path towards the instrument DLL. - """ - - (super().__init__)(name, **kwargs) - - self._devices_number = ctypes.c_uint() #setting the d - self._pxi10Enable = 0 #I don't know what this does - self._lock_external = 0 #This might set the default to internal clock refrence - self._clock_frequency = 10 #sets the clock frequency to 10 MHz - - self._serial_number = ctypes.c_char_p(bytes(serial_number, 'utf-8')) - - buffers = [ctypes.create_string_buffer(MAXDESCRIPTORSIZE + 1) for bid in range(MAXDEVICES)] - #this line creates a buffer (mutable memory) object for all the potential devices - self.buffer_pointer_array = (ctypes.c_char_p * MAXDEVICES)() - #c_char_p creates an array of C char types with null pointers of the size MAXDEVICES - for device in range(MAXDEVICES): - self.buffer_pointer_array[device] = ctypes.cast(buffers[device], ctypes.c_char_p) - #turning the elements of the buffer_pointer_array into char_p, which are pointers to strings - self._buffer_pointer_array_p = ctypes.cast(self.buffer_pointer_array, ctypes.POINTER(ctypes.c_char_p)) - # This defines the pointers of the buffer_pointer_array. I think - # Adapt the path to the computer language - if sys.platform == 'win32': #checks the OS - dll_path = os.path.join(os.environ['PROGRAMFILES'], dll_path)#adding the c:\ProgramFiles to the DLL path - self._dll = ctypes.WinDLL(dll_path) - print(dll_path) - print(self._dll) - else: - raise EnvironmentError(f"{self.__class__.__name__} is supported only on Windows platform") - - found = self._dll.sc5520a_uhfsSearchDevices(COMMINTERFACE, self._buffer_pointer_array_p, ctypes.byref(self._devices_number)) - - #runs the SearchDevices command with the ouput being the pointer to the serial numbers located in _buffer_pointer_array_p - if found: - raise RuntimeError('Failed to find any device') - self._open(serial_number) - #setting up retrieving the device status, required for changing just one of the elements of the ListModeT - self._list_mode = ListModeT() - self._status = OperateStatusT() - self._pll_status = PLLStatusT() - self._device_status = DeviceStatusT(self._list_mode, self._status, self._pll_status) - - self.add_parameter(name='temperature', - docstring='Return the microwave source internal temperature.', - label='Device temperature', - unit='celsius', - get_cmd=self._get_temperature) - - self.add_parameter(name='output_status', - docstring='.', - vals=Enum(0, 1), - set_cmd=self._set_status, - get_cmd=self._get_status) - - self.add_parameter(name='power', - docstring='.', - label='Power', - unit='dbm', - set_cmd=self._set_power, - get_cmd=self._get_power) - - self.add_parameter(name='frequency', - docstring='.', - label='Frequency', - unit='Hz', - set_cmd=self._set_frequency, - get_cmd=self._get_frequency) - - self.add_parameter(name='rf1_mode', - docstring='0=single tone. 1=sweep', - vals=Enum(0,1), - # initial_value=0, - set_cmd=self._set_rf_mode, - get_cmd=self._get_rf_mode) - - self.add_parameter(name='clock_frequency', - docstring='Select the internal clock frequency, 10 or 100MHz.', - unit='MHz', - vals=Enum(10, 100), - # initial_value=10, - set_cmd=self._set_clock_frequency, - get_cmd=self._get_clock_frequency) - - self.add_parameter(name='clock_reference', - docstring='Select the clock reference, internal or external.', - vals=Enum('internal', 'external'), - # initial_value='internal', - set_cmd=self._set_clock_reference, - get_cmd=self._get_clock_reference) - - ##Things Randy Wrote Start point - - self.add_parameter(name='sweep_start_frequency', - label='sweep_start_frequency', - docstring='Frequency at the start of sweep. Hz', - get_cmd=self._get_sweep_start_frequency, - set_cmd=self._set_sweep_start_frequency, - unit='Hz', - vals=Numbers(min_value=160E6,max_value=40E9) - ) - self.add_parameter(name='sweep_stop_frequency', - label='sweep_stop_frequency', - docstring='Frequency at the end of sweep.', - get_cmd=self._get_sweep_stop_frequency, - set_cmd=self._set_sweep_stop_frequency, - unit='Hz', - vals=Numbers(min_value=160E6,max_value=40E9) - ) - self.add_parameter(name='sweep_step_frequency', - label='sweep_step_frequency', - docstring='Frequency at the end of sweep.', - get_cmd=self._get_sweep_step_frequency, - set_cmd=self._set_sweep_step_frequency, - unit='Hz', - vals=Numbers(min_value=0,max_value=40E9) - ) - self.add_parameter(name='sweep_dwell_time', - label='sweep_dwell_time', - docstring='time in between sweep points. Units of 500us', - get_cmd=self._get_sweep_dwell_time, - get_parser=int, - set_cmd=self._set_sweep_dwell_time, - set_parser=int, - unit='', - vals=Numbers(min_value=1) - ) - self.add_parameter(name='sweep_cycles', - label='sweep_cycles', - docstring='how many times sweep is repeated. 0 is infinite', - get_cmd=self._get_sweep_cycles, - get_parser=int, - set_cmd=self._set_sweep_cycles, - set_parser=int, - unit='', - vals=Ints(min_value=0), - # initial_value=1, - ) - self.add_parameter(name='rf_phase_ouput', - label='rf_phase_output', - docstring='Ajust the phase of signal on the output. Must be multiples of 0.1 degree', - get_cmd=self._get_rf_phase_output, - set_cmd=self._set_rf_phase_output, - unit='degrees', - vals=PermissiveMultiples(0.1), - ) - self.add_parameter(name='sss_mode', - label='sss_mode', - docstring='0 = List mode. Device gets its frequency points from the list buffer uploaded via LIST_BUFFER_WRITE register. 1 = Sweep mode. The device computes the frequency points using the Start, Stop and Step frequencies', - get_cmd=self._get_sweep_mode, - set_cmd=self._set_sweep_mode, - unit='', - vals=Enum(0,1), - # initial_value=1, - ) - self.add_parameter(name='sweep_dir', - label='sweep_dir', - docstring='0 = forwards sweep. 1 = Backwards sweep', - get_cmd=self._get_sweep_dir, - set_cmd=self._set_sweep_dir, - unit='', - vals=Enum(0,1), - # initial_value=0, - ) - self.add_parameter(name='tri_waveform', - label='tri_waveform', - docstring='0 = Sawtooth waveform. 1 = Triangular waveform', - get_cmd=self._get_tri_waveform, - set_cmd=self._set_tri_waveform, - unit='', - vals=Enum(0,1), - # initial_value=0, - ) - self.add_parameter(name='hw_trigger', - label='hw_trigger', - docstring='0 = software trigger. 1 = hardware trigger', - get_cmd=self._get_hw_trigger, - set_cmd=self._set_hw_trigger, - unit='', - vals=Enum(0,1), - # initial_value=0, - ) - self.add_parameter(name='step_on_hw_trig', - label='step_on_hw_trig', - docstring='0 = start/stop. 1 =step to next freq. with hardware trigger', - get_cmd=self._get_step_on_hw_trig, - set_cmd=self._set_step_on_hw_trig, - unit='', - vals=Enum(0,1), - # initial_value=0, - ) - self.add_parameter(name='return_to_start', - label='return_to_start', - docstring='0=stops at end of list. 1=return to start of list at end', - get_cmd=self._get_return_to_start, - set_cmd=self._set_return_to_start, - unit='', - vals=Enum(0,1), - # initial_value=0, - ) - self.add_parameter(name='trig_out_enable', - label='trig_out_enable', - docstring='0=no trigger output. 1=trigger on TRIGOUT pin', - get_cmd=self._get_trig_out_enable, - set_cmd=self._set_trig_out_enable, - unit='', - vals=Enum(0,1), - # initial_value=1, - ) - self.add_parameter(name='trig_out_on_cycle', - label='trig_out_on_cycle', - docstring='0=trigger on frequency change. 1=trigger on cycle end', - get_cmd=self._get_trig_out_on_cycle, - set_cmd=self._set_trig_out_on_cycle, - unit='', - vals=Enum(0,1), - # initial_value=1, - ) - - self.connect_message() #Sends out a message that things have been connected - - def _open(self, serial_number) -> None: - if sys.platform == "win32": - self._handle = ctypes.wintypes.HANDLE() - else: - raise EnvironmentError(f"{self.__class__.__name__} is supported only on Windows platform") - - msg=self._dll.sc5520a_uhfsOpenDevice(COMMINTERFACE, self.buffer_pointer_array[0], ctypes.c_uint8(1), ctypes.byref(self._handle)) - # msg=self._dll.sc5520a_uhfsOpenDevice(COMMINTERFACE, #which communication interface we are using - # ctypes.c_char_p(bytes(serial_number, 'utf-8')), #serial number? - # ctypes.c_uint8(1), - # ctypes.byref(self._handle)) - self._error_handler(msg) - print(self._handle) - - def _close(self) -> None: - msg=self._dll.sc5520a_uhfsCloseDevice(self._handle) #closes the device - self._error_handler(msg) - def _error_handler(self, msg: int) -> None: - """Display error when setting the device fail. - - Args: - msg (int): error key, see error_dict dict. - - Raises: - BaseException - """ - - if msg!=0: - raise BaseException("Couldn't set the devise due to {}.".format(error_dict[str(msg)])) - else: - pass - - def soft_trigger(self) -> None: - """ - Send out a soft trigger, so that the we can start the sweep - Generator need to be configured for list mode and soft trigger is selected as the trigger source - """ - # logging.info(__name__ + ' : Send a soft trigger to the generator') - self._dll.sc5520a_uhfsListSoftTrigger(self._handle) - return None - - - def _get_temperature(self) -> float: - temperature = ctypes.c_float() - self._dll.sc5520a_uhfsFetchTemperature(self._handle, ctypes.byref(temperature)) - return temperature.value - - def _set_status(self, status_: int) -> None: - msg = self._dll.sc5520a_uhfsSetOutputEnable(self._handle, ctypes.c_int(status_)) - self._error_handler(msg) - - def _get_status(self) -> str: - self._dll.sc5520a_uhfsFetchDeviceStatus(self._handle, ctypes.byref(device_status_t)) - return device_status_t.operate_status_t.output_enable - - def _set_power(self, power: float) -> None: - msg = self._dll.sc5520a_uhfsSetPowerLevel(self._handle, ctypes.c_float(power)) - self._error_handler(msg) - - def _get_power(self) -> float: - self._dll.sc5520a_uhfsFetchRfParameters(self._handle, ctypes.byref(device_rf_params_t)) - return device_rf_params_t.power_level - - def _set_frequency(self, frequency: float) -> None: - msg = self._dll.sc5520a_uhfsSetFrequency(self._handle, ctypes.c_double(frequency)) - self._error_handler(msg) - - def _get_frequency(self) -> float: - device_rf_params_t = DeviceRFParamsT() - self._dll.sc5520a_uhfsFetchRfParameters(self._handle, ctypes.byref(device_rf_params_t)) - return float(device_rf_params_t.frequency) - - def _set_clock_frequency(self, clock_frequency: float) -> None: - if clock_frequency == 10: - self._select_high = 0 - else: - self._select_high = 1 - msg = self._dll.sc5520a_uhfsSetReferenceMode(self._handle, ctypes.c_int(self._pxi10Enable), ctypes.c_int(self._select_high), ctypes.c_int(self._lock_external)) - self._error_handler(msg) - - def _get_clock_frequency(self) -> float: - self._dll.sc5520a_uhfsFetchDeviceStatus(self._handle, ctypes.byref(device_status_t)) - ref_out_select = device_status_t.operate_status_t.ref_out_select - if ref_out_select == 1: - return 100 - return 10 - - def _set_clock_reference(self, clock_reference: str) -> None: - if clock_reference.lower() == 'internal': - self._lock_external = 0 - else: - self._lock_external = 1 - msg = self._dll.sc5520a_uhfsSetReferenceMode(self._handle, ctypes.c_int(self._pxi10Enable), ctypes.c_int(self._select_high), ctypes.c_int(self._lock_external)) - self._error_handler(msg) - - def _get_clock_reference(self) -> str: - self._dll.sc5520a_uhfsFetchDeviceStatus(self._handle, ctypes.byref(device_status_t)) - ext_ref_detect = device_status_t.operate_status_t.ext_ref_detect - if ext_ref_detect == 1: - return 'external' - return 'internal' - - def _set_rf_mode(self, rf_mode: int) -> None: - c_rf_mode = ctypes.c_ubyte(int(rf_mode)) - msg = self._dll.sc5520a_uhfsSetRfMode(self._handle, c_rf_mode) - self._error_handler(msg) - - def _get_rf_mode(self) -> str: - self._dll.sc5520a_uhfsFetchDeviceStatus(self._handle, ctypes.byref(device_status_t)) - rf_mode = device_status_t.operate_status_t.rf_mode - return int(rf_mode) - - #Methods I, peasant Randy, have defined, so probably don't work - def _set_sweep_start_frequency(self, sweep_start_freq: float) -> None: - """ - Set the start frequency of a sweep. Units of Hz. - """ - c_sweep_start_freq = ctypes.c_double(int(sweep_start_freq)) - msg = self._dll.sc5520a_uhfsSweepStartFreq(self._handle, c_sweep_start_freq) - self._error_handler(msg) - def _get_sweep_start_frequency(self) -> str: - """ - Set the start frequency of a sweep. Units of Hz. - """ - device_rf_params_t = DeviceRFParamsT() - self._dll.sc5520a_uhfsFetchRfParameters(self._handle, ctypes.byref(device_rf_params_t)) - return float(device_rf_params_t.sweep_start_freq) - - def _set_sweep_stop_frequency(self, sweep_stop_freq: float) -> None: - """ - Set the stop frequency of a sweep. Units of Hz. - """ - c_sweep_stop_freq = ctypes.c_double(int(sweep_stop_freq)) - msg = self._dll.sc5520a_uhfsSweepStopFreq(self._handle, c_sweep_stop_freq) - self._error_handler(msg) - def _get_sweep_stop_frequency(self) -> str: - """ - Set the start frequency of a sweep. Units of Hz. - """ - device_rf_params_t = DeviceRFParamsT() - self._dll.sc5520a_uhfsFetchRfParameters(self._handle, ctypes.byref(device_rf_params_t)) - return float(device_rf_params_t.sweep_stop_freq) - - def _set_sweep_step_frequency(self, sweep_step_freq: float) -> None: - """ - Set the frequency steps of a sweep. Units of Hz. - """ - c_sweep_step_freq = ctypes.c_double(int(sweep_step_freq)) - msg = self._dll.sc5520a_uhfsSweepStepFreq(self._handle, c_sweep_step_freq) - self._error_handler(msg) - def _get_sweep_step_frequency(self) -> str: - """ - Set the frequency steps of a sweep. Units of Hz. - """ - device_rf_params_t = DeviceRFParamsT() - self._dll.sc5520a_uhfsFetchRfParameters(self._handle, ctypes.byref(device_rf_params_t)) - return float(device_rf_params_t.sweep_step_freq) - - def _set_sweep_dwell_time(self, dwell_unit: int) -> None: - """ - Set the sweep/list time at each frequency point. - Note that the dwell time is set as multiple of 500 us. - The input value is an unsigned int, it means how many multiple of 500 us. - """ - c_dwell_unit = ctypes.c_uint(int(dwell_unit)) - msg = self._dll.sc5520a_uhfsSweepDwellTime(self._handle, c_dwell_unit) - self._error_handler(msg) - def _get_sweep_dwell_time(self) -> str: - """ - Get the sweep/list time at each frequency point. - Note that the dwell time is set as multiple of 500 us. - """ - device_rf_params_t = DeviceRFParamsT() - self._dll.sc5520a_uhfsFetchRfParameters(self._handle, ctypes.byref(device_rf_params_t)) - return float(device_rf_params_t.sweep_dwell_time) - - def _set_sweep_cycles(self, sweep_cycles: int) -> None: - """ - Set the number of times the sweep will cycle before stopping. - 0 corresponds to a an infinite loop - """ - c_sweep_cycles = ctypes.c_uint(int(sweep_cycles)) #making this a python int should be redudant - msg = self._dll.sc5520a_uhfsListCycleCount(self._handle, c_sweep_cycles) - self._error_handler(msg) - def _get_sweep_cycles(self) -> str: - """ - Get the number of times the sweep will cycle before stopping. - 0 corresponds to a an infinite loop - """ - device_rf_params_t = DeviceRFParamsT() - self._dll.sc5520a_uhfsFetchRfParameters(self._handle, ctypes.byref(device_rf_params_t)) - return int(device_rf_params_t.sweep_cycles) - - def _set_rf_phase_output(self, rf_phase_output:float) -> None: - """ - Sets the phase of the output RF signal. 0.1 degree steps - """ - c_rf_phase_output = ctypes.cfloat(rf_phase_output) - msg = self._dll.sc5520a_uhfsSetSignalPhase(self._handle, c_rf_phase_output) - self._error_handler(msg) - def _get_rf_phase_output(self) -> str: - """ - Sets the phase of the output RF signal. - """ - device_rf_params_t = DeviceRFParamsT() - self._dll.sc5520a_uhfsFetchRfParameters(self._handle, ctypes.byref(device_rf_params_t)) - return float(device_rf_params_t.rf_phase_offset) - - def _set_sweep_mode(self, sweep_mode:int) -> None: - """ - Set the list/sweep mode of the generator - 0 = List mode. Device gets its frequency points from the list buffer uploaded via LIST_BUFFER_WRITE register - 1 = Sweep mode. The device computes the frequency points using the Start, Stop and Step frequencies - """ - c_sweep_mode = ctypes.c_ubyte(int(sweep_mode)) #convert the Python Int into C byte - self._dll.sc5520a_uhfsFetchDeviceStatus(self._handle, ctypes.byref(self._device_status)) - #fetch the device status, which contains the list_mode_t values - self._device_status.list_mode_t.sweep_mode = c_sweep_mode - # change the dictonary value of "sweep mode" in the internally stored list mode dictionary - msg = self._dll.sc5520a_uhfsListModeConfig(self._handle, ctypes.byref(self._device_status.list_mode_t)) - - - self._error_handler(msg) - def _get_sweep_mode(self) -> str: - """ - Get the list/sweep mode of the generator - 0 = List mode. Device gets its frequency points from the list buffer uploaded via LIST_BUFFER_WRITE register - 1 = Sweep mode. The device computes the frequency points using the Start, Stop and Step frequencies - """ - self._dll.sc5520a_uhfsFetchDeviceStatus(self._handle, ctypes.byref(self._device_status)) - #fetch the device status, which contains the list_mode_t values - sweep_mode = self._device_status.list_mode_t.sweep_mode - return sweep_mode - def _set_sweep_dir(self, sweep_dir:int) -> None: - """ - Defines the sweep direction. - 0: Forewards sweep. - 1: Backwards sweep. - """ - c_sweep_dir = ctypes.c_ubyte(int(sweep_dir)) #convert the Python Int into C byte - self._dll.sc5520a_uhfsFetchDeviceStatus(self._handle, ctypes.byref(self._device_status)) - #fetch the device status, which contains the list_mode_t values - self._device_status.list_mode_t.sweep_dir = c_sweep_dir - # change the dictonary value of "sweep mode" in the internally stored list mode dictionary - msg = self._dll.sc5520a_uhfsListModeConfig(self._handle, ctypes.byref(self._device_status.list_mode_t)) - self._error_handler(msg) - def _get_sweep_dir(self) -> str: - """ - Defines the sweep direction. - 0: Forewards sweep. - 1: Backwards sweep. - """ - self._dll.sc5520a_uhfsFetchDeviceStatus(self._handle, ctypes.byref(self._device_status)) - #fetch the device status, which contains the list_mode_t values - sweep_dir = self._device_status.list_mode_t.sweep_dir - return sweep_dir - - def _set_tri_waveform(self, tri_waveform:int) -> None: - """ - Set the triangular waveform of the generator - 0 = Sawtooth waveform. Frequency returns to the beginning frequency upon reaching the end of a sweep cycle - 1 = Triangular waveform. Frequency reverses direction at the end of the list and steps back towards the - beginning to complete a cycle - """ - c_tri_waveform = ctypes.c_ubyte(int(tri_waveform)) #convert the Python Int into C byte - self._dll.sc5520a_uhfsFetchDeviceStatus(self._handle, ctypes.byref(self._device_status)) - #fetch the device status, which contains the list_mode_t values - self._device_status.list_mode_t.tri_waveform = c_tri_waveform - # change the dictonary value of "sweep mode" in the internally stored list mode dictionary - msg = self._dll.sc5520a_uhfsListModeConfig(self._handle, ctypes.byref(self._device_status.list_mode_t)) - self._error_handler(msg) - def _get_tri_waveform(self) -> str: - """ - Get the triangular waveform of the generator - 0 = Sawtooth waveform. Frequency returns to the beginning frequency upon reaching the end of a sweep cycle - 1 = Triangular waveform. Frequency reverses direction at the end of the list and steps back towards the - beginning to complete a cycle - """ - self._dll.sc5520a_uhfsFetchDeviceStatus(self._handle, ctypes.byref(self._device_status)) - #fetch the device status, which contains the list_mode_t values - tri_waveform = self._device_status.list_mode_t.tri_waveform - return tri_waveform - - def _set_hw_trigger(self, hw_trigger:int) -> None: - """ - Set the status of hardware trigger - 0 = Software trigger. Softtrigger can only be used to start and stop a sweep/list cycle. It does not work for - step-on-trigger mode. - 1 = Hardware trigger. A high-to-low transition on the TRIGIN pin will trigger the device. It can be used for - both start/stop or step-on-trigger functions. - """ - c_hw_trigger = ctypes.c_ubyte(int(hw_trigger)) #convert the Python Int into C byte - self._dll.sc5520a_uhfsFetchDeviceStatus(self._handle, ctypes.byref(self._device_status)) - #fetch the device status, which contains the list_mode_t values - self._device_status.list_mode_t.hw_trigger = c_hw_trigger - # change the dictonary value of "sweep mode" in the internally stored list mode dictionary - msg = self._dll.sc5520a_uhfsListModeConfig(self._handle, ctypes.byref(self._device_status.list_mode_t)) - self._error_handler(msg) - def _get_hw_trigger(self) -> str: - """ - Get the status of hardware trigger - 0 = Software trigger. Softtrigger can only be used to start and stop a sweep/list cycle. It does not work for - step-on-trigger mode. - 1 = Hardware trigger. A high-to-low transition on the TRIGIN pin will trigger the device. It can be used for - both start/stop or step-on-trigger functions. - """ - self._dll.sc5520a_uhfsFetchDeviceStatus(self._handle, ctypes.byref(self._device_status)) - #fetch the device status, which contains the list_mode_t values - hw_trigger = self._device_status.list_mode_t.hw_trigger - return hw_trigger - - def _set_step_on_hw_trig(self, step_on_hw_trig:int) -> None: - """ - Set the behavior of the sweep/list mode when receiving a trigger. - 0 = Start/Stop behavior. The sweep starts and continues to step through the list for the number of cycles set, - dwelling at each step frequency for a period set by the dwell time. The sweep/list will end on a consecutive - trigger. - 1 = Step-on-trigger. This is only available if hardware triggering is selected. The device will step to the next - frequency on a trigger.Upon completion of the number of cycles, the device will exit from the stepping state - and stop. - """ - c_step_on_hw_trig = ctypes.c_ubyte(int(step_on_hw_trig)) #convert the Python Int into C byte - self._dll.sc5520a_uhfsFetchDeviceStatus(self._handle, ctypes.byref(self._device_status)) - #fetch the device status, which contains the list_mode_t values - self._device_status.list_mode_t.step_on_hw_trig = c_step_on_hw_trig - # change the dictonary value of "sweep mode" in the internally stored list mode dictionary - msg = self._dll.sc5520a_uhfsListModeConfig(self._handle, ctypes.byref(self._device_status.list_mode_t)) - self._error_handler(msg) - def _get_step_on_hw_trig(self) -> str: - """ - Get the behavior of the sweep/list mode when receiving a trigger. - 0 = Start/Stop behavior - 1 = Step-on-trigger - """ - self._dll.sc5520a_uhfsFetchDeviceStatus(self._handle, ctypes.byref(self._device_status)) - #fetch the device status, which contains the list_mode_t values - step_on_hw_trig = self._device_status.list_mode_t.step_on_hw_trig - return step_on_hw_trig - - def _set_return_to_start(self, return_to_start:int) -> None: - """ - Set how the frequency will change at the end of the list/sweep - 0 = Stop at end of sweep/list. The frequency will stop at the last point of the sweep/list - 1 = Return to start. The frequency will return and stop at the beginning point of the sweep or list after a - cycle. - """ - c_return_to_start = ctypes.c_ubyte(int(return_to_start)) #convert the Python Int into C byte - self._dll.sc5520a_uhfsFetchDeviceStatus(self._handle, ctypes.byref(self._device_status)) - #fetch the device status, which contains the list_mode_t values - self._device_status.list_mode_t.return_to_start = c_return_to_start - # change the dictonary value of "sweep mode" in the internally stored list mode dictionary - msg = self._dll.sc5520a_uhfsListModeConfig(self._handle, ctypes.byref(self._device_status.list_mode_t)) - self._error_handler(msg) - def _get_return_to_start(self) -> str: - """ - Get how the frequency will change at the end of the list/sweep - 0 = Stop at end of sweep/list. The frequency will stop at the last point of the sweep/list - 1 = Return to start. The frequency will return and stop at the beginning point of the sweep or list after a - cycle. - """ - self._dll.sc5520a_uhfsFetchDeviceStatus(self._handle, ctypes.byref(self._device_status)) - #fetch the device status, which contains the list_mode_t values - return_to_start = self._device_status.list_mode_t.return_to_start - return return_to_start - - def _set_trig_out_enable(self, trig_out_enable:int) -> None: - """ - Set the trigger output status. - It does not send out the trigger, just enable the generator to send out the trigger - 0 = No trigger output - 1 = Puts a trigger pulse on the TRIGOUT pin - """ - c_trig_out_enable = ctypes.c_ubyte(int(trig_out_enable)) #convert the Python Int into C byte - self._dll.sc5520a_uhfsFetchDeviceStatus(self._handle, ctypes.byref(self._device_status)) - #fetch the device status, which contains the list_mode_t values - self._device_status.list_mode_t.trig_out_enable = c_trig_out_enable - # change the dictonary value of "sweep mode" in the internally stored list mode dictionary - msg = self._dll.sc5520a_uhfsListModeConfig(self._handle, ctypes.byref(self._device_status.list_mode_t)) - self._error_handler(msg) - def _get_trig_out_enable(self) -> str: - """ - Get the trigger output status. - It does not send out the trigger, just enable the generator to send out the trigger - 0 = No trigger output - 1 = Puts a trigger pulse on the TRIGOUT pin - """ - self._dll.sc5520a_uhfsFetchDeviceStatus(self._handle, ctypes.byref(self._device_status)) - #fetch the device status, which contains the list_mode_t values - trig_out_enable = self._device_status.list_mode_t.trig_out_enable - return trig_out_enable - - def _set_trig_out_on_cycle(self, trig_out_on_cycle:int) -> None: - """ - Set the trigger output mode - 0 = Puts out a trigger pulse at each frequency change - 1 = Puts out a trigger pulse at the completion of each sweep/list cycle - """ - c_trig_out_on_cycle = ctypes.c_ubyte(int(trig_out_on_cycle)) #convert the Python Int into C byte - self._dll.sc5520a_uhfsFetchDeviceStatus(self._handle, ctypes.byref(self._device_status)) - #fetch the device status, which contains the list_mode_t values - self._device_status.list_mode_t.trig_out_on_cycle = c_trig_out_on_cycle - # change the dictonary value of "sweep mode" in the internally stored list mode dictionary - msg = self._dll.sc5520a_uhfsListModeConfig(self._handle, ctypes.byref(self._device_status.list_mode_t)) - self._error_handler(msg) - def _get_trig_out_on_cycle(self) -> str: - """ - Get the trigger output mode - 0 = Puts out a trigger pulse at each frequency change - 1 = Puts out a trigger pulse at the completion of each sweep/list cycle - """ - self._dll.sc5520a_uhfsFetchDeviceStatus(self._handle, ctypes.byref(self._device_status)) - #fetch the device status, which contains the list_mode_t values - trig_out_on_cycle = self._device_status.list_mode_t.trig_out_on_cycle - return trig_out_on_cycle - - def get_idn(self) -> Dict[str, Optional[str]]: - self._dll.sc5520a_uhfsFetchDeviceInfo(self._handle, ctypes.byref(device_info_t)) - - return {'vendor':'SignalCore', - 'model':'SC5521A', - 'serial':device_info_t.product_serial_number, - 'firmware':device_info_t.firmware_revision, - 'hardware':device_info_t.hardware_revision, - 'manufacture_date':'20{}-{}-{} at {}h'.format(device_info_t.man_date.year, device_info_t.man_date.month, device_info_t.man_date.day, device_info_t.man_date.hour)} \ No newline at end of file diff --git a/labcore/instruments/qcodes_drivers/SignalCore/__init__.py b/labcore/instruments/qcodes_drivers/SignalCore/__init__.py deleted file mode 100644 index dea24dd..0000000 --- a/labcore/instruments/qcodes_drivers/SignalCore/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Wed May 20 09:12:21 2020 - -@author: Ryan K -""" - - diff --git a/labcore/instruments/qcodes_drivers/SignalHound/Spike.py b/labcore/instruments/qcodes_drivers/SignalHound/Spike.py deleted file mode 100644 index ea96ba1..0000000 --- a/labcore/instruments/qcodes_drivers/SignalHound/Spike.py +++ /dev/null @@ -1,109 +0,0 @@ -"""Basic driver to communicate with the SPIKE program through SCPI.""" - - -__author__ = "Michael Mollenhauer, Abdullah Irfan" -__email__ = "mcm16@illinois.edu, irfan3@illinois.edu" - - -import logging - -import numpy as np -import qcodes -from qcodes import (VisaInstrument, validators as vals) - - -class Spike(VisaInstrument): - """ - Pfafflab SignalHound Driver using the qcodes package - - """ - - def __init__(self, name, address=None, **kwargs): - if address is None: - raise Exception('TCP IP address needed') - logging.info(__name__ + ' : Initializing instrument Spike') - - super().__init__(name, address, terminator='\n', **kwargs) - - # Checks and changes the mode - self.add_parameter('mode', - get_cmd=':INSTRUMENT?', - set_cmd='INSTRUMENT {}', - vals=vals.Anything(), - get_parser=str, - ) - - - # Zero-span mode - # Changes the reference level in zero span mode - self.add_parameter('zs_ref_level', - get_cmd=':ZS:CAPTURE:RLEVEL?', - set_cmd=':ZS:CAPTURE:RLEVEL {}', - vals=vals.Numbers(), - get_parser=float, - unit='dB' - ) - - # Changes the center frequency in zero span mode - self.add_parameter('zs_fcenter', - get_cmd=':ZS:CAPTURE:CENTER?', - set_cmd=':ZS:CAPTURE:CENTER {}', - vals=vals.Numbers(), - get_parser=float, - unit='Hz' - ) - - # Changes the sampling rate in zero span mode - self.add_parameter('zs_sample_rate', - get_cmd=':ZS:CAPTURE:SRATE {}', - set_cmd=':ZS:CAPTURE:SRATE?', - vals=vals.Numbers(), - get_parser=float, - ) - - # Changes the IF bandwidth in zero span mode, only works when AUTO is off - self.add_parameter('zs_ifbw', - get_cmd=':ZS:CAPTURE:IFBWIDTH?', - set_cmd=':ZS:CAPTURE:IFBWIDTH {}', - vals=vals.Numbers(), - get_parser=float, - unit='Hz' - ) - - # Enables the AUTO IF bandwidth option in zero span mode - self.add_parameter('zs_ifbw_auto', - get_cmd=':ZS:CAPTURE:IFBWIDTH:AUTO?', - set_cmd=':ZS:CAPTURE:IFBWIDTH:AUTO {}', - vals=vals.Anything() - ) - - # Changes the sweep time in zero span mode - self.add_parameter('zs_sweep_time', - get_cmd=':ZS:CAPTURE:SWEEP:TIME?', - set_cmd=':ZS:CAPTURE:SWEEP:TIME {}', - vals=vals.Numbers(), - get_parser=float, - unit='s' - ) - - self.add_parameter('zs_power', - get_cmd=self._measure_zs_power_dBm, - set_cmd=False, - unit='dBm') - - self.add_parameter('zs_iq_values', - get_cmd=self._measure_zs_iq_vals, - set_cmd=False, - unit='dBm^.5') - - # setting defaults - self.mode('ZS') - - def _measure_zs_iq_vals(self): - IQ_table = np.array(self.ask(':FETCH:ZS? 1').split(',')).astype(float).reshape(-1, 2) - return IQ_table - - def _measure_zs_power_dBm(self): - IQ_table = np.array(self.ask(':FETCH:ZS? 1').split(',')).astype(float).reshape(-1, 2) - power = (IQ_table[:, 0] ** 2 + IQ_table[:, 1] ** 2).mean() - return 10 * np.log10(power) \ No newline at end of file diff --git a/labcore/instruments/qcodes_drivers/SignalHound/__init__.py b/labcore/instruments/qcodes_drivers/SignalHound/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/labcore/instruments/qcodes_drivers/ThorLabs/TSP_01B.py b/labcore/instruments/qcodes_drivers/ThorLabs/TSP_01B.py deleted file mode 100644 index 1f2f25b..0000000 --- a/labcore/instruments/qcodes_drivers/ThorLabs/TSP_01B.py +++ /dev/null @@ -1,78 +0,0 @@ -"""Basic driver to access the ThorLabs TSP-01B temperature sensor probe (TSP) via qcodes""" - - -__author__ = "Owen Stephenson" -__email__ = "owends2@illinois.edu" - -import logging -import qcodes -from qcodes import (Instrument, validators as vals) -from py_thorlabs_tsp import ThorlabsTsp01B - -class ThorLabs_TSP01B(Instrument): - - def __init__(self, name, serial=None, **kwargs): - - if serial is None: - raise Exception('TSP01 serial number needed!') - - logging.info(__name__ + f' : Initializing instrument TSP01 {serial}') - super().__init__(name, **kwargs) - - # serial number on sensor - self.sensor = ThorlabsTsp01B(serial) - - # first temperature measure (sensor inside USB device) - self.add_parameter('temp1', - get_cmd=self.measure_temp1, - set_cmd=False, - vals=vals.Numbers(), - get_parser=float, - unit='C') - - # second temperature measure (first external sensor) - self.add_parameter('temp2', - get_cmd=self.measure_temp2, - set_cmd=False, - vals=vals.Numbers(), - get_parser=float, - unit='C') - - # third temperature measure (second external sensor) - NOT USED CURRENTLY - self.add_parameter('temp3', - get_cmd=self.measure_temp3, - set_cmd=False, - vals=vals.Numbers(), - get_parser=float, - unit='C') - - # humidity measure (sensor inside USB device) - # units of relative humidity (percentage value) - self.add_parameter('humidity', - get_cmd=self.measure_humid, - set_cmd=False, - vals=vals.Numbers(), - get_parser=float, - unit='RH') - - self.connect_message() - - def get_idn(self): - return { - "vendor": "ThorLabs", - "model:": "TSP-01B", - "serial": "N/A (check device)", - "firmware": "N/A (check device)" - } - - def measure_temp1(self): - return self.sensor.measure_temperature('th0') - - def measure_temp2(self): - return self.sensor.measure_temperature('th1') - - def measure_temp3(self): - return self.sensor.measure_temperature('th2') - - def measure_humid(self): - return self.sensor.measure_humidity() diff --git a/labcore/instruments/qcodes_drivers/ThorLabs/__init__.py b/labcore/instruments/qcodes_drivers/ThorLabs/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/labcore/instruments/qcodes_drivers/Yokogawa/GS200.py b/labcore/instruments/qcodes_drivers/Yokogawa/GS200.py deleted file mode 100644 index 1047cd3..0000000 --- a/labcore/instruments/qcodes_drivers/Yokogawa/GS200.py +++ /dev/null @@ -1,644 +0,0 @@ -from functools import partial -from typing import Optional, Union, Any - -from qcodes.instrument.parameter import DelegateParameter -from qcodes.instrument.visa import VisaInstrument -from qcodes.instrument.channel import InstrumentChannel -from qcodes.utils.validators import Numbers, Bool, Enum, Ints - - -def float_round(val: float) -> int: - """ - Rounds a floating number - - Args: - val: number to be rounded - - Returns: - Rounded integer - """ - return round(float(val)) - - -class GS200Exception(Exception): - pass - - -class GS200_Monitor(InstrumentChannel): - """ - Monitor part of the GS200. This is only enabled if it is - installed in the GS200 (it is an optional extra). - - The units will be automatically updated as required. - - To measure: - `GS200.measure.measure()` - - Args: - parent (GS200) - name: instrument name - present - """ - def __init__(self, parent: 'GS200', name: str, present: bool) -> None: - super().__init__(parent, name) - - self.present = present - - # Start off with all disabled - self._enabled = False - self._output = False - - # Set up mode cache. These will be filled in once the parent - # is fully initialized. - self._range: Union[None, float] = None - self._unit: Union[None, str] = None - - # Set up monitoring parameters - if present: - self.add_parameter('enabled', - label='Measurement Enabled', - get_cmd=self.state, - set_cmd=lambda x: self.on() if x else self.off(), - val_mapping={ - 'off': 0, - 'on': 1, - }) - - # Note: Measurement will only run if source and - # measurement is enabled. - self.add_parameter('measure', - label='', unit='V/I', - get_cmd=self._get_measurement, - snapshot_get=False) - - self.add_parameter('NPLC', - label='NPLC', - unit='1/LineFreq', - vals=Ints(1, 25), - set_cmd=':SENS:NPLC {}', - set_parser=int, - get_cmd=':SENS:NPLC?', - get_parser=float_round) - self.add_parameter('delay', - label='Measurement Delay', - unit='ms', - vals=Ints(0, 999999), - set_cmd=':SENS:DEL {}', - set_parser=int, - get_cmd=':SENS:DEL?', - get_parser=float_round) - self.add_parameter('trigger', - label='Trigger Source', - set_cmd=':SENS:TRIG {}', - get_cmd=':SENS:TRIG?', - val_mapping={ - 'READY': 'READ', - 'READ': 'READ', - 'TIMER': 'TIM', - 'TIM': 'TIM', - 'COMMUNICATE': 'COMM', - 'IMMEDIATE': 'IMM', - 'IMM': 'IMM' - }) - self.add_parameter('interval', - label='Measurement Interval', - unit='s', - vals=Numbers(0.1, 3600), - set_cmd=':SENS:INT {}', - set_parser=float, - get_cmd=':SENS:INT?', - get_parser=float) - - def off(self) -> None: - """Turn measurement off""" - self.write(':SENS 0') - self._enabled = False - - def on(self) -> None: - """Turn measurement on""" - self.write(':SENS 1') - self._enabled = True - - def state(self) -> int: - """Check measurement state""" - state = int(self.ask(':SENS?')) - self._enabled = bool(state) - return state - - def _get_measurement(self) -> float: - if self._unit is None or self._range is None: - raise GS200Exception("Measurement module not initialized.") - if self._parent.auto_range.get() or (self._unit == 'VOLT' - and self._range < 1): - # Measurements will not work with autorange, or when - # range is <1V. - self._enabled = False - raise GS200Exception("Measurements will not work when range is <1V" - "or when in auto range mode.") - if not self._output: - raise GS200Exception("Output is off.") - if not self._enabled: - raise GS200Exception("Measurements are disabled.") - # If enabled and output is on, then we can perform a measurement. - return float(self.ask(':MEAS?')) - - def update_measurement_enabled(self, unit: str, - output_range: float) -> None: - """ - Args: - unit - output_range - """ - # Recheck measurement state next time we do a measurement - self._enabled = False - - # Update units - self._range = output_range - self._unit = unit - if self._unit == 'VOLT': - self.measure.label = 'Source Current' - self.measure.unit = 'I' - else: - self.measure.label = 'Source Voltage' - self.measure.unit = 'V' - - -class GS200Program(InstrumentChannel): - """ - """ - def __init__(self, parent: 'GS200', name: str) -> None: - super().__init__(parent, name) - self._repeat = 1 - self._file_name = None - - self.add_parameter("interval", - label="the program interval time", - unit='s', - vals=Numbers(0.1, 3600.0), - get_cmd=":PROG:INT?", - set_cmd=":PROG:INT {}") - - self.add_parameter("slope", - label="the program slope time", - unit='s', - vals=Numbers(0.1, 3600.0), - get_cmd=":PROG:SLOP?", - set_cmd=":PROG:SLOP {}") - - self.add_parameter("trigger", - label="the program trigger", - get_cmd=":PROG:TRIG?", - set_cmd=":PROG:TRIG {}", - vals=Enum('normal', 'mend')) - - self.add_parameter("save", - set_cmd=":PROG:SAVE '{}'", - docstring="save the program to the system memory " - "(.csv file)") - - self.add_parameter("load", - get_cmd=":PROG:LOAD?", - set_cmd=":PROG:LOAD '{}'", - docstring="load the program (.csv file) from the " - "system memory") - - self.add_parameter("repeat", - label="program execution repetition", - get_cmd=":PROG:REP?", - set_cmd=":PROG:REP {}", - val_mapping={'OFF': 0, - 'ON': 1}) - self.add_parameter("count", - label="step of the current program", - get_cmd=":PROG:COUN?", - set_cmd=":PROG:COUN {}", - vals=Ints(1, 10000)) - - self.add_function('start', - call_cmd=":PROG:EDIT:STAR", - docstring="start program editing") - self.add_function('end', - call_cmd=":PROG:EDIT:END", - docstring="end program editing") - self.add_function('run', - call_cmd=":PROG:RUN", - docstring="run the program",) - - -class GS200(VisaInstrument): - """ - This is the QCoDeS driver for the Yokogawa GS200 voltage and current source. - - Args: - name: What this instrument is called locally. - address: The GPIB or USB address of this instrument - kwargs: kwargs to be passed to VisaInstrument class - terminator: read terminator for reads/writes to the instrument. - """ - - def __init__(self, name: str, address: str, terminator: str = "\n", - **kwargs: Any) -> None: - super().__init__(name, address, terminator=terminator, **kwargs) - - self.add_parameter('output', - label='Output State', - get_cmd=self.state, - set_cmd=lambda x: self.on() if x else self.off(), - val_mapping={ - 'off': 0, - 'on': 1, - }) - - self.add_parameter('source_mode', - label='Source Mode', - get_cmd=':SOUR:FUNC?', - set_cmd=self._set_source_mode, - vals=Enum('VOLT', 'CURR')) - - # We need to get the source_mode value here as we cannot rely on the - # default value that may have been changed before we connect to the - # instrument (in a previous session or via the frontpanel). - self.source_mode() - - self.add_parameter('voltage_range', - label='Voltage Source Range', - unit='V', - get_cmd=partial(self._get_range, "VOLT"), - set_cmd=partial(self._set_range, "VOLT"), - vals=Enum(10e-3, 100e-3, 1e0, 10e0, 30e0), - snapshot_exclude=self.source_mode() == 'CURR' - ) - - self.add_parameter('current_range', - label='Current Source Range', - unit='I', - get_cmd=partial(self._get_range, "CURR"), - set_cmd=partial(self._set_range, "CURR"), - vals=Enum(1e-3, 10e-3, 100e-3, 200e-3), - snapshot_exclude=self.source_mode() == "VOLT" - ) - - self.add_parameter('range', - parameter_class=DelegateParameter, - source=None - ) - - # The instrument does not support auto range. The parameter - # auto_range is introduced to add this capability with - # setting the initial state at False mode. - self.add_parameter('auto_range', - label='Auto Range', - set_cmd=self._set_auto_range, - get_cmd=None, - initial_cache_value=False, - vals=Bool() - ) - - self.add_parameter('voltage', - label='Voltage', - unit='V', - set_cmd=partial(self._get_set_output, "VOLT"), - get_cmd=partial(self._get_set_output, "VOLT"), - snapshot_exclude=self.source_mode() == "CURR" - ) - - self.add_parameter('current', - label='Current', - unit='I', - set_cmd=partial(self._get_set_output, "CURR"), - get_cmd=partial(self._get_set_output, "CURR"), - snapshot_exclude=self.source_mode() == 'VOLT' - ) - - self.add_parameter('output_level', - parameter_class=DelegateParameter, - source=None - ) - - # We need to pass the source parameter for delegate parameters - # (range and output_level) here according to the present - # source_mode. - if self.source_mode() == 'VOLT': - self.range.source = self.voltage_range - self.output_level.source = self.voltage - else: - self.range.source = self.current_range - self.output_level.source = self.current - - self.add_parameter('voltage_limit', - label='Voltage Protection Limit', - unit='V', - vals=Ints(1, 30), - get_cmd=":SOUR:PROT:VOLT?", - set_cmd=":SOUR:PROT:VOLT {}", - get_parser=float_round, - set_parser=int) - - self.add_parameter('current_limit', - label='Current Protection Limit', - unit='I', - vals=Numbers(1e-3, 200e-3), - get_cmd=":SOUR:PROT:CURR?", - set_cmd=":SOUR:PROT:CURR {:.3f}", - get_parser=float, - set_parser=float) - - self.add_parameter('four_wire', - label='Four Wire Sensing', - get_cmd=':SENS:REM?', - set_cmd=':SENS:REM {}', - val_mapping={ - 'off': 0, - 'on': 1, - }) - - # Note: The guard feature can be used to remove common mode noise. - # Read the manual to see if you would like to use it - self.add_parameter('guard', - label='Guard Terminal', - get_cmd=':SENS:GUAR?', - set_cmd=':SENS:GUAR {}', - val_mapping={'off': 0, - 'on': 1}) - - # Return measured line frequency - self.add_parameter("line_freq", - label='Line Frequency', - unit="Hz", - get_cmd="SYST:LFR?", - get_parser=int) - - # Check if monitor is present, and if so enable measurement - monitor_present = '/MON' in self.ask("*OPT?") - measure = GS200_Monitor(self, 'measure', monitor_present) - self.add_submodule('measure', measure) - - # Reset function - self.add_function('reset', call_cmd='*RST') - - self.add_submodule('program', GS200Program(self, 'program')) - - self.add_parameter("BNC_out", - label="BNC trigger out", - get_cmd=":ROUT:BNCO?", - set_cmd=":ROUT:BNCO {}", - vals=Enum("trigger", "output", "ready"), - docstring="Sets or queries the output BNC signal") - - self.add_parameter("BNC_in", - label="BNC trigger in", - get_cmd=":ROUT:BNCI?", - set_cmd=":ROUT:BNCI {}", - vals=Enum("trigger", "output"), - docstring="Sets or queries the input BNC signal") - - self.add_parameter( - "system_errors", - get_cmd=":SYSTem:ERRor?", - docstring="returns the oldest unread error message from the event " - "log and removes it from the log." - ) - - self.connect_message() - - def on(self) -> None: - """Turn output on""" - self.write('OUTPUT 1') - self.measure._output = True - - def off(self) -> None: - """Turn output off""" - self.write('OUTPUT 0') - self.measure._output = False - - def state(self) -> int: - """Check state""" - state = int(self.ask('OUTPUT?')) - self.measure._output = bool(state) - return state - - def ramp_voltage(self, ramp_to: float, step: float, delay: float) -> None: - """ - Ramp the voltage from the current level to the specified output. - - Args: - ramp_to: The ramp target in Volt - step: The ramp steps in Volt - delay: The time between finishing one step and - starting another in seconds. - """ - self._assert_mode("VOLT") - self._ramp_source(ramp_to, step, delay) - - def ramp_current(self, ramp_to: float, step: float, delay: float) -> None: - """ - Ramp the current from the current level to the specified output. - - Args: - ramp_to: The ramp target in Ampere - step: The ramp steps in Ampere - delay: The time between finishing one step and starting - another in seconds. - """ - self._assert_mode("CURR") - self._ramp_source(ramp_to, step, delay) - - def _ramp_source(self, ramp_to: float, step: float, delay: float) -> None: - """ - Ramp the output from the current level to the specified output - - Args: - ramp_to: The ramp target in volts/amps - step: The ramp steps in volts/ampere - delay: The time between finishing one step and - starting another in seconds. - """ - saved_step = self.output_level.step - saved_inter_delay = self.output_level.inter_delay - - self.output_level.step = step - self.output_level.inter_delay = delay - self.output_level(ramp_to) - - self.output_level.step = saved_step - self.output_level.inter_delay = saved_inter_delay - - def _get_set_output(self, mode: str, - output_level: Optional[float] = None - ) -> Optional[float]: - """ - Get or set the output level. - - Args: - mode: "CURR" or "VOLT" - output_level: If missing, we assume that we are getting the - current level. Else we are setting it - """ - self._assert_mode(mode) - if output_level is not None: - self._set_output(output_level) - return None - return float(self.ask(":SOUR:LEV?")) - - def _set_output(self, output_level: float) -> None: - """ - Set the output of the instrument. - - Args: - output_level: output level in Volt or Ampere, depending - on the current mode. - """ - auto_enabled = self.auto_range() - - if not auto_enabled: - self_range = self.range() - if self_range is None: - raise RuntimeError("Trying to set output but not in" - " auto mode and range is unknown.") - else: - mode = self.source_mode.get_latest() - if mode == "CURR": - self_range = 200E-3 - else: - self_range = 30.0 - - # Check we are not trying to set an out of range value - if self.range() is None or abs(output_level)\ - > abs(self_range): - # Check that the range hasn't changed - if not auto_enabled: - self_range = self.range.get_latest() - if self_range is None: - raise RuntimeError("Trying to set output but not in" - " auto mode and range is unknown.") - # If we are still out of range, raise a value error - if abs(output_level) > abs(self_range): - raise ValueError("Desired output level not in range" - " [-{self_range:.3}, {self_range:.3}]". - format(self_range=self_range)) - - if auto_enabled: - auto_str = ":AUTO" - else: - auto_str = "" - cmd_str = f":SOUR:LEV{auto_str} {output_level:.5e}" - self.write(cmd_str) - - def _update_measurement_module(self, source_mode: Optional[str] = None, - source_range: Optional[float] = None - ) -> None: - """ - Update validators/units as source mode/range changes. - - Args: - source_mode: "CURR" or "VOLT" - source_range - """ - if not self.measure.present: - return - - if source_mode is None: - source_mode = self.source_mode.get_latest() - # Get source range if auto-range is off - if source_range is None and not self.auto_range(): - source_range = self.range() - - self.measure.update_measurement_enabled(source_mode, source_range) - - def _set_auto_range(self, val: bool) -> None: - """ - Enable/disable auto range. - - Args: - val: auto range on or off - """ - self._auto_range = val - # Disable measurement if auto range is on - if self.measure.present: - # Disable the measurement module if auto range is enabled, - # because the measurement does not work in the - # 10mV/100mV ranges. - self.measure._enabled &= not val - - def _assert_mode(self, mode: str) -> None: - """ - Assert that we are in the correct mode to perform an operation. - - Args: - mode: "CURR" or "VOLT" - """ - if self.source_mode.get_latest() != mode: - raise ValueError("Cannot get/set {} settings while in {} mode". - format(mode, self.source_mode.get_latest())) - - def _set_source_mode(self, mode: str) -> None: - """ - Set output mode and change delegate parameters' source accordingly. - Also, exclude/include the parameters from snapshot depending on the - mode. The instrument does not support 'current', 'current_range' - parameters in "VOLT" mode and 'voltage', 'voltage_range' parameters - in "CURR" mode. - - Args: - mode: "CURR" or "VOLT" - - """ - if self.output() == 'on': - raise GS200Exception("Cannot switch mode while source is on") - - if mode == "VOLT": - self.range.source = self.voltage_range - self.output_level.source = self.voltage - self.voltage_range.snapshot_exclude = False - self.voltage.snapshot_exclude = False - self.current_range.snapshot_exclude = True - self.current.snapshot_exclude = True - else: - self.range.source = self.current_range - self.output_level.source = self.current - self.voltage_range.snapshot_exclude = True - self.voltage.snapshot_exclude = True - self.current_range.snapshot_exclude = False - self.current.snapshot_exclude = False - - self.write(f"SOUR:FUNC {mode}") - # We set the cache here since `_update_measurement_module` - # needs the current value which would otherwise only be set - # after this method exits - self.source_mode.cache.set(mode) - # Update the measurement mode - self._update_measurement_module(source_mode=mode) - - def _set_range(self, mode: str, output_range: float) -> None: - """ - Update range - - Args: - mode: "CURR" or "VOLT" - output_range: Range to set. For voltage, we have the ranges [10e-3, - 100e-3, 1e0, 10e0, 30e0]. For current, we have the ranges [1e-3, - 10e-3, 100e-3, 200e-3]. If auto_range = False, then setting the - output can only happen if the set value is smaller than the - present range. - """ - self._assert_mode(mode) - output_range = float(output_range) - self._update_measurement_module(source_mode=mode, - source_range=output_range) - self.write(f':SOUR:RANG {output_range}') - - def _get_range(self, mode: str) -> float: - """ - Query the present range. - - Args: - mode: "CURR" or "VOLT" - - Returns: - range: For voltage, we have the ranges [10e-3, 100e-3, 1e0, 10e0, - 30e0]. For current, we have the ranges [1e-3, 10e-3, 100e-3, - 200e-3]. If auto_range = False, then setting the output can only - happen if the set value is smaller than the present range. - """ - self._assert_mode(mode) - return float(self.ask(":SOUR:RANG?")) diff --git a/labcore/instruments/qcodes_drivers/Yokogawa/__init__.py b/labcore/instruments/qcodes_drivers/Yokogawa/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/labcore/instruments/qcodes_drivers/__init__.py b/labcore/instruments/qcodes_drivers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/labcore/instruments/qick/__init__.py b/labcore/instruments/qick/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/labcore/instruments/qick/config.py b/labcore/instruments/qick/config.py deleted file mode 100755 index 9932069..0000000 --- a/labcore/instruments/qick/config.py +++ /dev/null @@ -1,75 +0,0 @@ -import logging - -import Pyro4 -from qick import QickConfig - - -logger = logging.getLogger(__name__) - -class QBoardConfig: - - - def __init__(self, params, nameserver_host="192.168.1.1", nameserver_port=8888, nameserver_name="rfsoc"): - """ - - Check the reference, https://qick-docs.readthedocs.io/en/latest/_autosummary/qick.qick_asm.html#qick.qick_asm.QickConfig - for some useful conversion function that exist - - - :param params: Proxy instance of the parameter manager to to get values. - :param nameserver_host: IP of the host nameserver. This is usually the measurement PC you are using. - :param nameserver_port: Port of the host nameserver. - :param nameserver_name: Name of the nameserver. This needs to match between the qick and measurement computer - - """ - self.params = params - self.nameserver_host = nameserver_host - self.nameserver_port = nameserver_port - self.nameserver_name = nameserver_name - self.soc = None - self.soccfg = None # Network configuration - - - def generate_soccfg(self) -> QickConfig: - """ - Generates the network configuration from the nameserver. Sotres the soc and soccfg in the class. - - :return: soccfg: QickConfig instance. - """ - - Pyro4.config.SERIALIZER = 'pickle' - Pyro4.config.PICKLE_PROTOCOL_VERSION = 4 - ns = Pyro4.locateNS(host=self.nameserver_host, port=self.nameserver_port) - soc = Pyro4.Proxy(ns.lookup(self.nameserver_name)) - soccfg = QickConfig(soc.get_cfg()) - self.soc = soc - self.soccfg = soccfg - logger.info("Generated soccfg") - logger.info(soccfg) - return soccfg - - - # TODO: See if there is a way of checking if there has to be some way of checking for the required parameters like reps and expts that some qick classes require. - def config(self): - """ - Generates the configuration and the updates configuration of the qick. - - Returns both the network configuration and the qick configuration. - Both are needed to start a measurement. - """ - if self.soc is None or self.soccfg is None: - self.generate_soccfg() - - conf = self.config_() - # If you are using the averager program this needs to be a part of the config - if "reps" not in conf: - try: - conf["reps"] = self.params.reps() - except Exception as e: - raise AttributeError("Could not get reps from parameter manager, please provide reps in the configuration or parameter manager") - - - return self.soccfg, conf - - def config_(self): - raise NotImplementedError("config_() method must be implemented in subclass") \ No newline at end of file diff --git a/labcore/instruments/qick/qick_sweep_v2.py b/labcore/instruments/qick/qick_sweep_v2.py deleted file mode 100755 index c6142b0..0000000 --- a/labcore/instruments/qick/qick_sweep_v2.py +++ /dev/null @@ -1,143 +0,0 @@ -""" Tools to enable the usage of QICK in the Sweep framework. - -Required packages/hardware: -- FPGA configured with QICK -- QICK package - -Example usage of this module: ->>> @QickBoardSweep( ->>> independent("freqs"), ->>> ComplexQICKData("signal", ->>> depends_on=["freqs"], ->>> i_data_stream='I', q_data_stream='Q'), ->>> ) ->>> class SingleToneSpecstroscopyProgram(RAveragerProgram): - -To run any measurement, you would need the followign parameters in your config -n_echoes -steps -reps -final_delay -""" - -import numpy as np -from collections.abc import Iterable, Generator -from dataclasses import dataclass - -from labcore.measurement import independent, dependent, DataSpec -from labcore.measurement.sweep import AsyncRecord -from labcore.measurement.record import make_data_spec - -# config has to be set by the users of the program. -# Example: -# conf = QickConfig(paras=params) -# qick_sweep.config = conf -config = None - -@dataclass -class ComplexQICKData(DataSpec): - i_data_stream: str = 'I' - q_data_stream: str = 'Q' - - -@dataclass -class PulseVariable(DataSpec): - pulse_parameter: str = None - sweep_parameter: str = None - -@dataclass -class TimeVariable(DataSpec): - time_parameter: str = None - - - -class QickBoardSweep(AsyncRecord): - """ - Decorator class to communicate with QICK in the Sweeping framework. - """ - def __init__(self, *specs, **kwargs): - """ - Initialize the decorator class by saving incoming DataSpec variables. - """ - self.communicator = {} - self.specs = [] - for s in specs: - spec = make_data_spec(s) - self.specs.append(spec) - - def setup(self, func, *args, **kwargs): - """ - Setup a QICK program. - """ - # Checks that the config is not None - if config is None: - raise Exception("QickSweep: config is not set") - - self.config = config - conf = config.config() - qick_program = func(soccfg=conf[0], reps = conf[1]['reps'], final_delay=conf[1]['final_delay'], cfg=conf[1]) - self.communicator["qick_program"] = qick_program - - def collect(self, *args, **kwargs): - """ - Get the measurement data. - Note that one can overload the given acquire function if one needs - to perform other specific tasks. e.g. Needs to plot each non-averaged - points in the I-Q plane for a readout fidelity experiment. - Assumptions - * Given DataSpecs are either independent, dependent, or ComplexQICKData. - """ - # TODO: How can I extend this to multiple measurement rounds? (e.g. active reset) - - # Run the program - data = self.communicator["qick_program"].acquire(self.config.soc, progress=False)[0] - cfg = self.config.config()[1] - return_data = {} - - measIdx = 0 # To specify the index of the measured data to return - sweepIdx = 0 # To specify the index of the sweep variable to return - for ds in self.specs: - if isinstance(ds, ComplexQICKData): - return_data[ds.name]= data[measIdx].dot([1,1j]) - measIdx += 1 - elif isinstance(ds, PulseVariable): - return_data[ds.name]= self.communicator["qick_program"].get_pulse_param(ds.pulse_parameter, ds.sweep_parameter, as_array=True) - sweepIdx += 1 - elif isinstance(ds, TimeVariable): - return_data[ds.name]= (self.communicator["qick_program"].get_time_param(ds.time_parameter, 't', as_array=True))*(cfg['n_echoes']+1) - sweepIdx += 1 - else: - return_data[ds.name] = np.arange(cfg['steps']) - sweepIdx += 1 - - # Reformat the independent variables - # Independent (sweep) variables have to be reformatted such that xArray and plottr can - # correctly recognize the axis being swept. Without reformatting the swept variable, - # the program won't be able to correctly set up the axis being swept. - shapeIdx = 0 - for ds in self.specs: - if isinstance(ds, ComplexQICKData): - return_data[ds.name] = np.transpose(return_data[ds.name]) - else: - dimList = [1] * sweepIdx - dimList[shapeIdx] = len(return_data[ds.name]) - dimTuple = tuple(dimList) - diffList = [1] * sweepIdx - diffIdx = 0 - for dp in self.specs: - if isinstance(dp, ComplexQICKData): - pass - else: - if diffIdx != shapeIdx: - diffList[diffIdx] = len(return_data[dp.name]) - diffIdx += 1 - diffTuple = tuple(diffList) - return_data[ds.name] = np.reshape(return_data[ds.name], dimTuple) - return_data[ds.name] = np.tile(return_data[ds.name], diffTuple) - - shapeIdx += 1 - - yield return_data - - - diff --git a/labcore/instruments/qick/startNameserver.sh b/labcore/instruments/qick/startNameserver.sh deleted file mode 100755 index 948972f..0000000 --- a/labcore/instruments/qick/startNameserver.sh +++ /dev/null @@ -1,22 +0,0 @@ -# try to find conda path and activate it -condapath="$HOME/anaconda3/bin" -if [ -d "/opt/anaconda3" ]; then - source /opt/anaconda3/bin/activate -elif [ -d "$condapath" ]; then - source $condapath/activate -else - echo "can't find conda" -fi -# Start a Pyro4 nameserver with pc hostname -export PYRO_SERIALIZERS_ACCEPTED=pickle -export PYRO_PICKLE_PROTOCOL_VERSION=4 -# find IPV4 address of the ethernet card on local network, -# find the local network IP by searching the one that starts with 192.168 -LOCALIPV4=$(ifconfig | grep -oE "inet (addr:)?([0-9]*\.){3}[0-9]*" | grep -oE "([0-9]*\.){3}[0-9]*" | grep -v '127.0.0.1' | grep '192.168') -pyro4-ns -n $LOCALIPV4 -p 8888 - -# using hostname doesn't guarantee that the IPv4 of local network will be used. -# hostname=$(hostname) -# pyro4-ns -n $hostname -p 8888 - -read -p "Press enter to continue" diff --git a/labcore/measurement/__init__.py b/labcore/measurement/__init__.py deleted file mode 100644 index 12744de..0000000 --- a/labcore/measurement/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .record import recording, record_as, DataSpec, ds, dep, indep, dependent, \ - independent, get_parameter -from .sweep import Sweep, sweep_parameter, once, pointer diff --git a/labcore/py.typed b/labcore/py.typed deleted file mode 100644 index 4ed7be9..0000000 --- a/labcore/py.typed +++ /dev/null @@ -1,2 +0,0 @@ -this file marks plottr as safe for typechecking -https://mypy.readthedocs.io/en/latest/installed_packages.html#installed-packages diff --git a/labcore/setup_measurements.py b/labcore/setup_measurements.py deleted file mode 100644 index 3173e2f..0000000 --- a/labcore/setup_measurements.py +++ /dev/null @@ -1,201 +0,0 @@ -import os -import sys -import logging -from typing import Optional, Any, Union, List, Dict, Tuple -from functools import partial -from dataclasses import dataclass -from pathlib import Path - -from instrumentserver.client import Client, ProxyInstrument -from instrumentserver.helpers import nestedAttributeFromString - -from .data.datadict import DataDict -from .data.datadict_storage import data_info -from .measurement.storage import run_and_save_sweep -from .measurement import Sweep -from .utils.misc import get_environment_packages, commit_changes_in_repo - -# constants -WD = os.getcwd() -DATADIR = os.path.join(WD, 'data') - - -@dataclass -class Options: - instrument_clients: Optional[Dict[str, Client]] = None - parameters: Optional[ProxyInstrument] = None - qubit_defaults: Optional[callable]= lambda: None - - -options = Options() - - -def param_from_name(name: str, ): - return nestedAttributeFromString(options.parameters, name) - - -def getp(name: str, default=None, raise_if_missing=False): - if options.parameters is None: - logger.error("No parameter manager defined. cannot get/set params!") - return None - - try: - p = param_from_name(name) - return p() - except AttributeError: - if raise_if_missing: - raise - else: - return default - - -# this function sets up our general logging -def setup_logging() -> logging.Logger: - """Setup logging in a reasonable way. Note: we use the root logger since - our measurements typically run in the console directly and we want - logging to work from scripts that are directly run in the console. - - Returns - ------- - The logger that has been setup. - """ - logger = logging.getLogger() - logger.setLevel(logging.INFO) - - for h in logger.handlers: - logger.removeHandler(h) - del h - - fmt = logging.Formatter( - "%(asctime)s.%(msecs)03d\t| %(name)s\t| %(levelname)s\t| %(message)s", - datefmt='%Y-%m-%d %H:%M:%S', - ) - fh = logging.FileHandler('measurement.log') - fh.setFormatter(fmt) - fh.setLevel(logging.INFO) - logger.addHandler(fh) - - fmt = logging.Formatter( - "[%(asctime)s.%(msecs)03d] [%(name)s: %(levelname)s] %(message)s", - datefmt='%Y-%m-%d %H:%M:%S', - ) - streamHandler = logging.StreamHandler(sys.stdout) - streamHandler.setFormatter(fmt) - streamHandler.setLevel(logging.INFO) - logger.addHandler(streamHandler) - logger.info(f"Logging set up for {logger}.") - return logger - -# Create the logger -logger = setup_logging() - -def find_or_create_remote_instrument(cli: Client, ins_name: str, ins_class: Optional[str]=None, - *args: Any, **kwargs: Any) -> ProxyInstrument: - """Finds or creates an instrument in an instrument server. - - Parameters - ---------- - cli - instance of the client pointing to the instrument server - ins_name - name of the instrument to find or to create - ins_class - the class of the instrument (import path as string) if creating a new instrument - args - will be passed to the instrument creation call - kwargs - will be passed to the instrument creation call - - Returns - ------- - Proxy to the remote instrument - """ - if ins_name in cli.list_instruments(): - return cli.get_instrument(ins_name) - - if ins_class is None: - raise ValueError('Need a class to create a new instrument') - - ins = cli.create_instrument( - instrument_class=ins_class, - name=ins_name, *args, **kwargs) - - return ins - - -def run_measurement(sweep: Sweep, name: str, safe_write_mode: bool = False, **kwargs) -> Tuple[Union[str, Path], Optional[DataDict]]: - """ - Wrapper function around run_and_save_sweep that makes sure you are saving your measurement with all the necessary - metadata around it. - This includes the current git commit hash, the python environment, and the snapshot of all - instruments and parameters. - It is considered bad practice to have uncommitted changes in a repository that is installed locally. - If you have to change something from a repo, please open up a PR, this usually means that something is wrong there. - - Assumptions - ----------- - - To use this function, you need: - * Have an instrumentserver instance running with an instantiated client in options.instrument_clients - * Have a parameter manager proxy instrument in options.parameters. You get this simply - by passing the usually called params variable to options. - - How-to set up auto-committing - --------------------------- - - To get this function to commit your measurement code before running the measurement, - you need to follow the following steps: - - * Run `git init` in the directory where you are running your measurements. - If you are getting an error saying "could not set core.filemode to 'false'", run `sudo git init` instead. - * In your measurement folder copy the file :doc:`../doc/example.gitignore` and rename it to '.gitignore'. - * Commit all the files in your measurement folder running the command `git add -a -m "Initial commit"`. - - After those 3 steps have been taken, every time a measurement is run, the folder should autocommit before any measurement. - """ - if options.instrument_clients is None: - raise RuntimeError('it looks like options.instrument_clients is not configured.') - if options.parameters is None: - raise RuntimeError('it looks like options.parameters is not configured.') - - for n, c in options.instrument_clients.items(): - # Signature for snapshot changed, we need to check for both now. - if hasattr(c, 'get_snapshot'): - kwargs[n] = c.get_snapshot - elif hasattr(c, 'snapshot'): - kwargs[n] = c.snapshot - else: - raise RuntimeError(f"Could not find snapshot method for client {n}. Please update all packages.") - - kwargs['parameters'] = options.parameters.toParamDict - - py_env = get_environment_packages() - - current_dir = Path.cwd() - commit_hash = commit_changes_in_repo(current_dir) - - if commit_hash is None: - logger.warning("The current directory is not a git repository, your measurement code will not be tracked.") - - save_kwargs = { - 'sweep': sweep, - 'data_dir': DATADIR, - 'name': name, - 'save_action_kwargs': True, - 'python_environment': py_env, - 'safe_write_mode': safe_write_mode, - **kwargs - } - if commit_hash is not None: - save_kwargs['current_commit'] = {"measurement-hash": commit_hash} - - data_location, data = run_and_save_sweep(**save_kwargs) - - logger.info(f""" -========== -Saved data at {data_location}: -{data_info(data_location, do_print=False)} -=========""") - return data_location, data - - diff --git a/labcore/setup_opx_measurements.py b/labcore/setup_opx_measurements.py deleted file mode 100644 index a5e3b68..0000000 --- a/labcore/setup_opx_measurements.py +++ /dev/null @@ -1,205 +0,0 @@ -"""general setup file for OPX measurements. - -Use by importing and then configuring the options object. -""" - -# this is to prevent the OPX logger to also create log messages (results in duplicate messages) -import os -os.environ['QM_DISABLE_STREAMOUTPUT'] = "1" - -from typing import Optional, Callable -from dataclasses import dataclass -from functools import partial - -from IPython.display import display -import ipywidgets as widgets - -# FIXME: only until everyone uses the latest qm packages. -try: - from qm.QuantumMachinesManager import QuantumMachinesManager, QuantumMachine -except: - from qm.quantum_machines_manager import QuantumMachinesManager - from qm import QuantumMachine - -from qm.qua import * - -from instrumentserver.helpers import nestedAttributeFromString - -from .instruments.opx.config import QMConfig -from .instruments.opx import sweep as qmsweep -from .instruments.opx.mixer import calibrate_mixer, MixerConfig, mixer_of_step, mixer_imb_step - -from . import setup_measurements -from .setup_measurements import * - -@dataclass -class Options(setup_measurements.Options): - _qm_config: Optional[QMConfig] = None - - # this is implemented as a property so we automatically set the - # options correctly everywhere else... - @property - def qm_config(self): - return self._qm_config - - @qm_config.setter - def qm_config(self, cfg): - self._qm_config = cfg - qmsweep.config = cfg - -options = Options() -setup_measurements.options = options - -@dataclass -class Mixer: - config: MixerConfig - qm: Optional[QuantumMachine] = None - qmm: Optional[QuantumMachinesManager] = None - - def run_constant_waveform(self, **kwargs): - """ - When using this with the octaves, the fields `cluster_name` and `octave` with their corresponding values need to be in the kwargs. - """ - try: - with program() as const_pulse: - with infinite_loop_(): - play('constant', self.config.element_name) - if self.config.qmconfig.cluster_name is not None: - qmm = QuantumMachinesManager(host=self.config.qmconfig.opx_address, - port=None, - cluster_name=self.config.qmconfig.cluster_name, - **kwargs) - else: - qmm = QuantumMachinesManager(host=self.config.qmconfig.opx_address, - port=self.config.qmconfig.opx_port, **kwargs) - self.qmm = qmm - qm = qmm.open_qm(self.config.qmconfig(), close_other_machines=False) - qm.execute(const_pulse) - self.qm = qm - except KeyError as e: - message = None - if len(kwargs) < 2: - message = "Seems like no arguments were passed. " \ - "If you are using the octaves you need to pass the arguments 'cluster_name' and 'octave', try passing them as keyqord arguments and try again." - raise AttributeError(message + f" Error raised was: {e}" ) - - - def step_of(self, di, dq): - if self.qm is None: - raise RuntimeError('No active QuantumMachine.') - mixer_of_step(self.config, self.qm, di, dq) - - def step_imb(self, dg, dp): - if self.qm is None: - raise RuntimeError('No active QuantumMachine.') - mixer_imb_step(self.config, self.qm, dg, dp) - -def add_mixer_config(qubit_name, analyzer, generator, readout=False, element_to_param_map=None, **config_kwargs): - """ - FIXME: add docu (@wpfff) - TODO: make sure we document the meaning of `element_to_param_map`. - - contributor(s): Michael Mollenhaur - - arguments: - qubit_name - string; name of the qubit listed in the parameter manager you are going to work with - analyzer - instrument; instrument module for the spectrum analyzer used for the mixer - generator - instrument; instrument module for the LO generator used for the mixer - readout - boolean; whether you are calibrating the readout mixer for the specified qubit - element_to_param_map - string; specifies whether to call the qubit or readout parameter manager values - - """ - element_name = qubit_name - if readout is True: - element_name += '_readout' - - if element_to_param_map is None: - element_to_param_map = qubit_name - - if readout is True: - element_to_param_map += '.readout' - - cfg = MixerConfig( - qmconfig=options.qm_config, - opx_address=options.qm_config.opx_address, - opx_port=options.qm_config.opx_port, - opx_cluster_name=options.qm_config.cluster_name, - analyzer=analyzer, - generator=generator, - if_param=nestedAttributeFromString(options.parameters, f"{element_to_param_map}.IF"), - offsets_param=nestedAttributeFromString(options.parameters, f"mixers.{element_to_param_map}.offsets"), - imbalances_param=nestedAttributeFromString(options.parameters, f"mixers.{element_to_param_map}.imbalance"), - mixer_name=f'{element_name}_IQ_mixer', - element_name=element_name, - pulse_name='constant', - **config_kwargs - ) - return Mixer( - config=cfg, - ) - - -# A simple graphical mixer tuning tool -def mixer_tuning_tool(mixer): - # widgets for dc offset tuning - of_step = widgets.FloatText(description='dc of. step:', value=0.01, min=0, max=1, step=0.001) - iup_btn = widgets.Button(description='I ^') - idn_btn = widgets.Button(description='I v') - qup_btn = widgets.Button(description='Q ^') - qdn_btn = widgets.Button(description='Q v') - - def on_I_up(b): - mixer.step_of(of_step.value, 0) - - def on_I_dn(b): - mixer.step_of(-of_step.value, 0) - - def on_Q_up(b): - mixer.step_of(0, of_step.value) - - def on_Q_dn(b): - mixer.step_of(0, -of_step.value) - - iup_btn.on_click(on_I_up) - idn_btn.on_click(on_I_dn) - qup_btn.on_click(on_Q_up) - qdn_btn.on_click(on_Q_dn) - - # widgets for imbalance tuning - imb_step = widgets.FloatText(description='imb. step:', value=0.01, min=0, max=1, step=0.001) - gup_btn = widgets.Button(description='g ^') - gdn_btn = widgets.Button(description='g v') - pup_btn = widgets.Button(description='phi ^') - pdn_btn = widgets.Button(description='phi v') - - def on_g_up(b): - mixer.step_imb(imb_step.value, 0) - - def on_g_dn(b): - mixer.step_imb(-imb_step.value, 0) - - def on_p_up(b): - mixer.step_imb(0, imb_step.value) - - def on_p_dn(b): - mixer.step_imb(0, -imb_step.value) - - gup_btn.on_click(on_g_up) - gdn_btn.on_click(on_g_dn) - pup_btn.on_click(on_p_up) - pdn_btn.on_click(on_p_dn) - - # assemble reasonably for display - ofupbox = widgets.HBox([iup_btn, qup_btn]) - ofdnbox = widgets.HBox([idn_btn, qdn_btn]) - ofbox = widgets.VBox([of_step, ofupbox, ofdnbox]) - - imbupbox = widgets.HBox([gup_btn, pup_btn]) - imbdnbox = widgets.HBox([gdn_btn, pdn_btn]) - imbbox = widgets.VBox([imb_step, imbupbox, imbdnbox]) - - box = widgets.HBox([ofbox, imbbox]) - display(box) - - -run_measurement = partial(run_measurement, qmconfig=lambda: options.qm_config() if options.qm_config is not None else None) diff --git a/labcore/testing/resonator_readout_data.py b/labcore/testing/resonator_readout_data.py deleted file mode 100755 index b2edc7c..0000000 --- a/labcore/testing/resonator_readout_data.py +++ /dev/null @@ -1,52 +0,0 @@ -from typing import Union -import numpy as np -from labcore.data.datadict import str2dd -from pprint import pprint - -# Define constants and parameters -amplitude = 2 # Amplitude of the resonator response -noise_level = 0.2 # Noise level - -# Simulate the resonator response -def simulate_S21(center_frequency, Q_factor, frequency_range, num_points): - frequencies = np.linspace(center_frequency - frequency_range / 2, center_frequency + frequency_range / 2, num_points) - response = amplitude / (1 + 1j * (frequencies - center_frequency) / (center_frequency / Q_factor)) - response += np.random.normal(0, noise_level, len(frequencies)) - return response, frequencies - - -def resonator_dataset(center_frequency, Q_factor, frequency_range, reps = 10, num_points: int = 100): - data = str2dd("signal(repetition, fs); fs[Hz]; testing[s];") - response, frequencies = simulate_S21(center_frequency, Q_factor,frequency_range, num_points) - for i in range(reps): - for j in range(100): - data.add_data( - signal=response, - fs = frequencies, - testing=[j], - repetition=np.arange(num_points, dtype=int)+1, - ) - return data -# Plot the resonator response - - -def plot_resonator_response(frequencies, response): - plt.figure() - plt.plot(frequencies, np.abs(response), label='Amplitude') - plt.xlabel('Frequency (Hz)') - plt.ylabel('Amplitude') - plt.legend() - plt.title('Microwave Resonator Response') - plt.grid(True) - plt.show() - -# Main function -if __name__ == '__main__': - from matplotlib import pyplot as plt - center_frequency = 5e9 # Center frequency in Hz - frequency_range = 1e9 # Frequency range in Hz - Q_factor = 100 - num_points = 2000 - response, frequencies = simulate_S21(center_frequency, Q_factor, frequency_range, num_points) - plot_resonator_response(frequencies, response) - diff --git a/labcore/utils/__init__.py b/labcore/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pyproject.toml b/pyproject.toml index e54e053..5706405 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,43 @@ +[project] +name = "labcore" +version = "1.0.0" +description = "toolkit for acquiring, processing, and analyzing data in a CM/QI physics lab." +readme = "README.md" +requires-python = ">=3.11" +license = "MIT" +authors = [ + {name = "Wolfgang Pfaff", email = "wolfgangpfff@gmail.com"}, + {name = "Marcos Frenkel", email = "marcosf2@illinois.edu"}, +] +dependencies = [ + "jupyterlab", + "jupyter_bokeh", + "qcodes", + "bokeh", + "pandas", + "xarray", + "matplotlib", + "numpy", + "seaborn", + "lmfit", + "h5py", + "holoviews", + "panel", + "param", + "hvplot", + "gitpython", + "watchdog", + "Pillow", + "nest_asyncio", + "ruamel.yaml", +] + + +[project.scripts] +autoplot = "labcore.scripts.monitr_server:run_autoplot" +reconstruct-data = "labcore.scripts.reconstruct_safe_write_data:main" + + [build-system] requires = [ "setuptools >= 48", @@ -6,7 +46,14 @@ requires = [ ] build-backend = 'setuptools.build_meta' +[tool.ruff] +exclude = ["docs"] + +[tool.ruff.lint] +extend-select = ["I"] + [tool.mypy] +files = ["src"] strict_optional = true show_column_numbers = true warn_unused_ignores = true @@ -14,7 +61,6 @@ warn_unused_configs = true warn_redundant_casts = true no_implicit_optional = true disallow_untyped_defs = true -plugins = "numpy.typing.mypy_plugin" show_error_codes = true enable_error_code = "ignore-without-code" @@ -24,13 +70,17 @@ module = [ "lmfit", "matplotlib.*", "pyqtgraph.*", - "xhistogram.*", "ruamel.*", "param.*", "holoviews.*", "hvplot.*", "bokeh.*", "setuptools.*", + "pandas", + "pandas.*", + "seaborn", + "seaborn.*", + "nest_asyncio", ] ignore_missing_imports = true @@ -38,39 +88,34 @@ ignore_missing_imports = true testpaths = ["test"] log_cli = true -[project] -name = "labcore" -version = "0.0.0" -description = "toolkit for acquiring, processing, and analyzing data in a CM/QI physics lab." -readme = "README.md" -requires-python = ">=3.10" -license = {text = "MIT"} -dependencies = [ - "jupyterlab", - "jupyter_bokeh", - "qcodes==0.44.1", - "bokeh", - "pandas", - "xarray", - "matplotlib", - "numpy==1.26.4", - "scipy", - "scikit-learn", - "seaborn", - "lmfit", - "h5py==3.10.0", - "xhistogram", - "holoviews", - "panel", - "param", - "hvplot", - "versioningit", - "qtpy", - "gitpython", - "watchdog", - "pywavelets", -] +[tool.coverage.run] +branch = true -[project.scripts] -autoplot = "scripts.monitr_server:run_autoplot" -pyro-ns = "scripts.pyro_nameserver:run_pyro_nameserver" \ No newline at end of file +[tool.setuptools] +package-dir = {"" = "src"} + +[tool.setuptools.packages.find] +include = ["labcore*"] +exclude = ["site*", "docs*", "test*"] + +[tool.setuptools.package-data] +labcore = ["py.typed"] + +[dependency-groups] +dev = [ + "pytest", + "pytest-cov", + "ruff", + "mypy", + "types-markdown>=3.10.2.20260211", +] +docs = [ + "sphinx", + "pydata-sphinx-theme", + "myst-parser", + "myst-nb", + "pandoc", + "ipykernel", + "linkify-it-py", + "h5py", +] diff --git a/scripts/__init__.py b/scripts/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/scripts/monitr_server.py b/scripts/monitr_server.py deleted file mode 100644 index 1a7c2f1..0000000 --- a/scripts/monitr_server.py +++ /dev/null @@ -1,57 +0,0 @@ -import argparse -import logging -from pathlib import Path - - -import panel as pn -pn.extension() - -from labcore.analysis.hvapps import DataSelect, DDH5LoaderNode - -logger = logging.getLogger(__file__) - -def make_template(data_root='.'): - ds = DataSelect(data_root) - loader = DDH5LoaderNode() - - def data_selected_cb(*events): - loader.file_path = events[0].new - - watch_data_selected = ds.param.watch(data_selected_cb, ['selected_path']) - - def refilter_data_select(*events): - ds.data_select() - - search_data_typed = ds.param.watch(refilter_data_select, ['search_term']) - - temp = pn.template.BootstrapTemplate( - site="labcore", - title="autoplot", - sidebar=[], - main=[ds, loader] - ) - - return temp - - -def run_autoplot(): - parser = argparse.ArgumentParser( - description="Data monitoring program made for Pfaff lab by Rocky Daehler, building" - " on Plottr made by Wolfgang Pfaff. Run command on it's own to start the" - " application, and pass an (optional) path to the data directory as a" - " second argument.") - parser.add_argument('Datapath', nargs='?', default='.') - - args = parser.parse_args() - - data_root = Path(args.Datapath) - if (not data_root.is_dir()): - logger.error("Provided Path was invalid.\nPlease provide a path to an existing directory housing your data.") - return - - logger.info(f"Running Labcore.Autoplot on data from {data_root}") - - template = make_template(data_root) - template.show() - -make_template(".").servable() diff --git a/scripts/pyro_nameserver.py b/scripts/pyro_nameserver.py deleted file mode 100644 index 02c43e5..0000000 --- a/scripts/pyro_nameserver.py +++ /dev/null @@ -1,71 +0,0 @@ -import argparse -import logging -import os -import sys - -# Configure logging to output to console -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' -) -logger = logging.getLogger(__name__) - - -def run_pyro_nameserver(): - """ - Run the Pyro4 nameserver directly in the terminal. - This allows stopping the server with Ctrl+C. - """ - parser = argparse.ArgumentParser( - description="Start Pyro4 nameserver for QICK integration. " - "The nameserver runs in the foreground and can be stopped with Ctrl+C." - ) - parser.add_argument( - '--host', - '-n', - default='localhost', - help='Host address for the nameserver (default: localhost)' - ) - parser.add_argument( - '--port', - '-p', - type=int, - default=8888, - help='Port number for the nameserver (default: 8888)' - ) - - args = parser.parse_args() - - # Set required environment variables for Pyro4 - os.environ["PYRO_SERIALIZERS_ACCEPTED"] = "pickle" - os.environ["PYRO_PICKLE_PROTOCOL_VERSION"] = "4" - - logger.info(f"Starting Pyro4 nameserver on {args.host}:{args.port}") - logger.info("Press Ctrl+C to stop the nameserver") - - # Build the command to execute pyro4-ns - cmd = [ - "pyro4-ns", - "-n", - args.host, - "-p", - str(args.port), - ] - - try: - # Replace the current process with pyro4-ns - # This will run pyro4-ns directly in the terminal - os.execvp("pyro4-ns", cmd) - except FileNotFoundError: - logger.error("pyro4-ns command not found. Please install Pyro4 with: pip install Pyro4") - sys.exit(1) - except Exception as e: - logger.error(f"Failed to start Pyro4 nameserver: {e}") - logger.error(f"Error type: {type(e).__name__}") - import traceback - traceback.print_exc() - sys.exit(1) - - -if __name__ == "__main__": - run_pyro_nameserver() \ No newline at end of file diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index bc9bb1a..0000000 --- a/setup.cfg +++ /dev/null @@ -1,55 +0,0 @@ -[metadata] -name = labcore -description = toolkit for acquiring, processing, and analyzing data in a CM/QI physics lab. -long_description = file: README.md -long_description_content_type = text/markdown -author = Wolfgang Pfaff -author_email = wolfgangpfff@gmail.com -classifiers = - Development Status :: 3 - Alpha - Intended Audience :: Science/Research - License :: OSI Approved :: MIT License - Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Topic :: Scientific/Engineering -license = MIT -url = https://github.com/toolsforexperiments/labcore -project_urls = - Source = https://github.com/toolsforexperiments/labcore - Tracker = https://github.com/toolsforexperiments/labcore - -[options] -packages = find: -python_requires = >=3.8 -install_requires = - pandas>=0.22 - xarray>=0.10.0 - scipy>=1.0 - panel>=0.11.3 - param>=1.10.1 - holoviews>=1.14.1 - hvplot>=0.7.2 - scikit-learn>=0.20.0 - bokeh>=2.0.0 - matplotlib>=3.0.0 - numpy>=1.12.0 - lmfit>=1.0 - h5py>=2.10.0 - typing-extensions>=3.7.4.3 - packaging>=20.0 - xhistogram>=0.3.0 - versioningit>=1.1.0 - gitpython>=3.1.0 - watchdog>=2.1.6 - -[options.package_data] -labcore = - py.typed - -[options.entry_points] -console_scripts = - reconstruct-data = scripts.reconstruct_safe_write_data:main - autoplot = scripts.monitr_server:run_autoplot - pyro-ns = scripts.pyro_nameserver:run_pyro_nameserver \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 4477377..0000000 --- a/setup.py +++ /dev/null @@ -1,13 +0,0 @@ -from setuptools import setup -from versioningit import get_cmdclasses - -if __name__ == "__main__": - setup( - cmdclass=get_cmdclasses(), - entry_points={ - 'console_scripts': [ - 'autoplot = scripts.monitr_server:run_autoplot', - 'pyro-ns = scripts.pyro_nameserver:run_pyro_nameserver', - ], - }, - ) diff --git a/labcore/__init__.py b/src/labcore/__init__.py similarity index 100% rename from labcore/__init__.py rename to src/labcore/__init__.py diff --git a/src/labcore/analysis/__init__.py b/src/labcore/analysis/__init__.py new file mode 100644 index 0000000..de0195c --- /dev/null +++ b/src/labcore/analysis/__init__.py @@ -0,0 +1,7 @@ +from .analysis_base import DatasetAnalysis as DatasetAnalysis +from .fit import Fit as Fit +from .fit import FitResult as FitResult +from .hvplotting import ComplexHist as ComplexHist +from .hvplotting import Node as Node +from .hvplotting import ValuePlot as ValuePlot +from .hvplotting import plot_data as plot_data diff --git a/labcore/analysis/analysis_base.py b/src/labcore/analysis/analysis_base.py similarity index 87% rename from labcore/analysis/analysis_base.py rename to src/labcore/analysis/analysis_base.py index 3b637f8..5b744b2 100644 --- a/labcore/analysis/analysis_base.py +++ b/src/labcore/analysis/analysis_base.py @@ -1,25 +1,22 @@ -from typing import Optional, Type, Any, Dict, List -from types import TracebackType -from pathlib import Path -from datetime import datetime import json import logging import pickle - -import numpy as np -from matplotlib.figure import Figure -from matplotlib import pyplot as plt -import xarray as xr -import pandas as pd +from datetime import datetime +from pathlib import Path +from types import TracebackType +from typing import Any, Dict, List, Optional, Type, Union # Needed to generate hvplot from a script -import hvplot.xarray import holoviews as hv +import numpy as np +import pandas as pd +import xarray as xr +from matplotlib import pyplot as plt +from matplotlib.figure import Figure from ..data.datadict_storage import NumpyEncoder, timestamp_from_path from .fit import AnalysisResult, FitResult - logger = logging.getLogger(__name__) @@ -28,25 +25,24 @@ class AnalysisExistsError(Exception): class DatasetAnalysis: - figure_save_format = ["png", "pdf"] raise_on_earlier_analysis = None def __init__( - self, datafolder, name, analysisfolder="./analysis/", has_period_in_name=False, - raise_on_earlier_analysis=None, - ): + self, + datafolder: Union[str, Path], + name: str, + analysisfolder: Union[str, Path] = "./analysis/", + has_period_in_name: bool = False, + raise_on_earlier_analysis: Any = None, + ) -> None: if raise_on_earlier_analysis is not None: self.raise_on_earlier_analysis = raise_on_earlier_analysis self.name = name # The folder that contains the data we are performing an analysis - self.datafolder = datafolder - if not isinstance(self.datafolder, Path): - self.datafolder = Path(self.datafolder) - self.analysisfolder = analysisfolder - if not isinstance(self.analysisfolder, Path): - self.analysisfolder = Path(self.analysisfolder) + self.datafolder: Path = Path(datafolder) + self.analysisfolder: Path = Path(analysisfolder) self.timestamp = str( datetime.now().replace(microsecond=0).isoformat().replace(":", "") ) @@ -65,10 +61,10 @@ def __init__( self.savefolders.append(f) - self.entities = {} - self.files = [] + self.entities: Dict[str, Any] = {} + self.files: List[Path] = [] - def __enter__(self): + def __enter__(self) -> "DatasetAnalysis": earlier_exist = False if self.raise_on_earlier_analysis is not None: earlier_exist = True @@ -96,9 +92,9 @@ def _new_file_path(self, folder: Path, name: str, suffix: str = "") -> Path: # --- loading measurement data --- # def load_metadata_from_json( self, - file_name, - key, - ): + file_name: str, + key: str, + ) -> Any: """Load a parameter from a metadata json file Parameters @@ -129,10 +125,10 @@ def load_metadata_from_json( def load_saved_parameter( self, - parameter_name, - parameter_manager_name="parameter_manager", - file_name="parameters.json", - ): + parameter_name: str, + parameter_manager_name: str = "parameter_manager", + file_name: str = "parameters.json", + ) -> Any: fn = self.datafolder / file_name with open(fn, "r") as f: @@ -153,7 +149,9 @@ def add(self, **kwargs: Any) -> None: ) self.entities[k] = v - def add_figure(self, name, *arg, fig: Optional[Figure] = None, **kwargs) -> Figure: + def add_figure( + self, name: str, *arg: Any, fig: Optional[Figure] = None, **kwargs: Any + ) -> Figure: if name in self.entities: raise ValueError("element with that name already exists in this analysis.") if fig is None: @@ -163,7 +161,7 @@ def add_figure(self, name, *arg, fig: Optional[Figure] = None, **kwargs) -> Figu make_figure = add_figure - def to_table(self, name, data: Dict[str, Any]): + def to_table(self, name: str, data: Dict[str, Any]) -> None: data.update( { "data_loc": self.datafolder.name, @@ -171,12 +169,14 @@ def to_table(self, name, data: Dict[str, Any]): } ) - def make_table(data): + def make_table(data: Dict[str, Any]) -> pd.DataFrame: row = {k: [v] for k, v in data.items()} index = row.pop("data_loc") return pd.DataFrame(row, index=index) - def append_to_table(df, data, must_match=False): + def append_to_table( + df: pd.DataFrame, data: Dict[str, Any], must_match: bool = False + ) -> pd.DataFrame: row = make_table(data) if must_match: if not np.all(row.columns == df.columns): @@ -204,19 +204,19 @@ def append_to_table(df, data, must_match=False): df.to_csv(path) @staticmethod - def load_table(path): + def load_table(path: Union[str, Path]) -> pd.DataFrame: df = pd.read_csv(path, index_col=0) df["datetime"] = pd.to_datetime(df["datetime"]) return df # --- Saving analysis results --- # - def save(self): + def save(self) -> None: for folder in self.savefolders: if not folder.exists(): folder.mkdir(parents=True, exist_ok=True) for name, element in self.entities.items(): - fp = None + fp: Optional[Union[Path, List[Path]]] = None try: if isinstance(element, Figure): fp = self.save_mpl_figure(element, name, folder) @@ -277,9 +277,9 @@ def save(self): try: n = name + f"_{str(type(element))}" fp = self.save_pickle(element, n, folder) - except: + except Exception: logger.error(f"Could not pickle {name}.") - except: + except Exception: logger.warning( f"data '{name}', type {type(element)}" f"could not be saved regularly, try pickle instead..." @@ -287,7 +287,7 @@ def save(self): try: n = name + f"_{str(type(element))}" fp = self.save_pickle(element, n, folder) - except: + except Exception: logger.error(f"Could not pickle {name}.") if fp is not None: @@ -362,7 +362,9 @@ def save_str(self, data: str, name: str, folder: Path) -> Path: f.write(data) return fp - def save_np(self, data: np.ndarray, name: str, folder: Path) -> Path: + def save_np( + self, data: Union[np.ndarray, int, float, complex], name: str, folder: Path + ) -> Path: fp = self._new_file_path(folder, name, "json") with open(fp, "x") as f: json.dump({name: data}, f, cls=NumpyEncoder) @@ -383,28 +385,28 @@ def save_df(self, data: pd.DataFrame, name: str, folder: Path) -> Path: data.to_csv(fp) return fp - def save_pickle(self, data: Any, name: str, folder: Path) -> Path: + def save_pickle(self, data: object, name: str, folder: Path) -> Path: fp = self._new_file_path(folder, name, "pickle") with open(fp, "wb") as f: pickle.dump(data, f, pickle.HIGHEST_PROTOCOL) return fp # --- loading (and managing) earlier analysis results --- # - def get_analysis_data_file(self, name: str, format=["json"]): + def get_analysis_data_file(self, name: str, format: List[str] = ["json"]) -> Path: files = list(self.savefolders[0].glob(f"*{name}*")) files = [f for f in files if f.suffix[1:] in format] if len(files) == 0: raise ValueError(f"no analysis data found for '{name}'") return files[-1] - def has_analysis_data(self, name: str, format=["json"]): + def has_analysis_data(self, name: str, format: List[str] = ["json"]) -> bool: try: self.get_analysis_data_file(name, format) return True except ValueError: return False - def load_analysis_data(self, name: str, format=["json"]): + def load_analysis_data(self, name: str, format: List[str] = ["json"]) -> Any: fp = self.get_analysis_data_file(name, format) with open(fp, "r") as f: data = json.load(f) diff --git a/labcore/analysis/fit.py b/src/labcore/analysis/fit.py similarity index 86% rename from labcore/analysis/fit.py rename to src/labcore/analysis/fit.py index 8675546..d621411 100644 --- a/labcore/analysis/fit.py +++ b/src/labcore/analysis/fit.py @@ -1,12 +1,9 @@ -from typing import Tuple, Any, Optional, Union, Dict, List, Type from collections import OrderedDict -from dataclasses import dataclass from pathlib import Path -import json +from typing import Any, Dict, List, Optional, Tuple, Type, Union -import numpy as np -from matplotlib import pyplot as plt import lmfit +import numpy as np import xarray as xr @@ -83,7 +80,7 @@ def analyze( coordinates: Union[Tuple[np.ndarray, ...], np.ndarray], data: np.ndarray, *args: Any, - **kwargs: Any + **kwargs: Any, ) -> AnalysisResult: """Needs to be implemented by each inheriting class.""" raise NotImplementedError @@ -113,7 +110,7 @@ def analyze( dry: bool = False, params: Dict[str, Any] = {}, *args: Any, - **fit_kwargs: Any + **fit_kwargs: Any, ) -> FitResult: model = lmfit.model.Model(self.model) @@ -146,7 +143,6 @@ def guess( def xr2fitinput(arr: xr.DataArray) -> Tuple[List[np.ndarray], np.ndarray]: - shp = arr.shape coords1d = (arr[k].values for k in arr.dims) coords = [a.flatten() for a in np.meshgrid(*coords1d, indexing="ij")] if len(coords) == 1: @@ -160,7 +156,7 @@ def fit_and_add_to_ds( dim_name: str, fit_class: Type[Fit], dim_order: Optional[List[int]] = None, - **run_kwargs: Any + **run_kwargs: Any, ) -> Tuple[xr.Dataset, FitResult]: arr = ds[dim_name] coords_, vals = xr2fitinput(arr) @@ -169,12 +165,13 @@ def fit_and_add_to_ds( else: coords = coords_ - fit = fit_class(coords, vals) + fit = fit_class(tuple(coords) if isinstance(coords, list) else coords, vals) result = fit.run(**run_kwargs) + assert isinstance(result, FitResult) fit_data = result.eval() fit_darr = xr.DataArray( - name=arr.name + "_fit", + name=str(arr.name) + "_fit", data=fit_data.reshape(arr.shape), dims=arr.dims, coords=arr.coords, @@ -189,44 +186,36 @@ def plot_ds_2d_with_fit( x: Optional[str] = None, y: Optional[str] = None, plot_kwargs: Dict[str, Any] = {}, -): +) -> Any: data = ds[dim_name] - fit = ds[dim_name + '_fit'] + fit = ds[dim_name + "_fit"] if x is None: - x = data.dims[0] + x = str(data.dims[0]) if y is None: - y = data.dims[1] + y = str(data.dims[1]) dataopts = dict( clim=(data.min(), data.max()), - cmap='magma', + cmap="magma", ) dataopts.update(**plot_kwargs) - - title = 'data' - folder = ds.attrs.get('raw_data_folder', None) + + title = "data" + folder = ds.attrs.get("raw_data_folder", None) if folder is not None: title += f": {Path(folder).stem}" - plot = data.hvplot.quadmesh( - x=x, - y=y, - title=title, - **dataopts - ) \ - + fit.hvplot.quadmesh( - x=x, - y=y, - title='fit', - **dataopts - ) \ - + (data - fit).hvplot.quadmesh( - x=x, - y=y, - title='residuals', - cmap='bwr', - **plot_kwargs, + plot = ( + data.hvplot.quadmesh(x=x, y=y, title=title, **dataopts) + + fit.hvplot.quadmesh(x=x, y=y, title="fit", **dataopts) + + (data - fit).hvplot.quadmesh( + x=x, + y=y, + title="residuals", + cmap="bwr", + **plot_kwargs, + ) ) - + return plot diff --git a/labcore/analysis/fitfuncs/__init__.py b/src/labcore/analysis/fitfuncs/__init__.py similarity index 100% rename from labcore/analysis/fitfuncs/__init__.py rename to src/labcore/analysis/fitfuncs/__init__.py diff --git a/labcore/analysis/fitfuncs/generic.py b/src/labcore/analysis/fitfuncs/generic.py similarity index 66% rename from labcore/analysis/fitfuncs/generic.py rename to src/labcore/analysis/fitfuncs/generic.py index 4524e8d..c14b86f 100644 --- a/labcore/analysis/fitfuncs/generic.py +++ b/src/labcore/analysis/fitfuncs/generic.py @@ -1,7 +1,6 @@ -from typing import Tuple, Any, Optional, Union, Dict, List +from typing import Dict, Tuple, Union import numpy as np -import lmfit from ..fit import Fit @@ -50,15 +49,17 @@ def guess( class ExponentialDecay(Fit): @staticmethod - def model(coordinates, A, of, tau) -> np.ndarray: + def model(coordinates: np.ndarray, A: float, of: float, tau: float) -> np.ndarray: """$A * \exp(-x/\tau) + of$""" - return A * np.exp(-coordinates/tau) + of + return A * np.exp(-coordinates / tau) + of @staticmethod - def guess(coordinates, data): - + def guess( + coordinates: Union[Tuple[np.ndarray, ...], np.ndarray], data: np.ndarray + ) -> Dict[str, float]: + assert isinstance(coordinates, np.ndarray) # offset guess: The mean of the last 10 percent of the data - of = np.mean(data[-data.size//10:]) + of = np.mean(data[-data.size // 10 :]) # amplitude guess: difference between max and min. A = np.abs(np.max(data) - np.min(data)) @@ -66,8 +67,8 @@ def guess(coordinates, data): A *= -1 # tau guess: pick the point where we reach roughly 1/e - one_over_e_val = of + A/3. - one_over_e_idx = np.argmin(np.abs(data-one_over_e_val)) + one_over_e_val = of + A / 3.0 + one_over_e_idx = np.argmin(np.abs(data - one_over_e_val)) tau = coordinates[one_over_e_idx] return dict(A=A, of=of, tau=tau) @@ -75,15 +76,19 @@ def guess(coordinates, data): class Linear(Fit): @staticmethod - def model(coordinates, m, of) -> np.ndarray: + def model(coordinates: np.ndarray, m: float, of: float) -> np.ndarray: """$A * \exp(-x/\tau) + of$""" return m * coordinates + of @staticmethod - def guess(coordinates, data): - + def guess( + coordinates: Union[Tuple[np.ndarray, ...], np.ndarray], data: np.ndarray + ) -> Dict[str, float]: + assert isinstance(coordinates, np.ndarray) # amplitude guess: difference between max and min y over the max and min x. - m = np.abs(np.max(data) - np.min(data))/np.abs(np.max(coordinates) - np.min(coordinates)) + m = np.abs(np.max(data) - np.min(data)) / np.abs( + np.max(coordinates) - np.min(coordinates) + ) # offset guess: how far shifted the linear function is along y of = data[0] - m * coordinates[0] @@ -93,25 +98,37 @@ def guess(coordinates, data): class ExponentiallyDecayingSine(Fit): @staticmethod - def model(coordinates, A, of, f, phi, tau) -> np.ndarray: + def model( + coordinates: np.ndarray, A: float, of: float, f: float, phi: float, tau: float + ) -> np.ndarray: """$A \sin(2*\pi*(f*x + \phi/360)) \exp(-x/\tau) + of$""" - return A * np.sin(2 * np.pi * (f * coordinates + phi/360)) * np.exp(-coordinates/tau) + of + return ( + A + * np.sin(2 * np.pi * (f * coordinates + phi / 360)) + * np.exp(-coordinates / tau) + + of + ) @staticmethod - def guess(coordinates, data): + def guess( + coordinates: Union[Tuple[np.ndarray, ...], np.ndarray], data: np.ndarray + ) -> Dict[str, float]: """This guess will ignore the first value because since it usually is not relaiable.""" + assert isinstance(coordinates, np.ndarray) # offset guess: The mean of the data of = np.mean(data) # amplitude guess: difference between max and min. - A = np.abs(np.max(data) - np.min(data)) / 2. + A = np.abs(np.max(data) - np.min(data)) / 2.0 if data[0] < data[-1]: A *= -1 # f guess: Maximum of the absolute value of the fourier transform. fft_data = np.fft.rfft(data)[1:] - fft_coordinates = np.fft.rfftfreq(data.size, coordinates[1] - coordinates[0])[1:] + fft_coordinates = np.fft.rfftfreq(data.size, coordinates[1] - coordinates[0])[ + 1: + ] # note to confirm, could there be multiple peaks? I am always taking the first one here. f_max_index = np.argmax(fft_data) @@ -121,8 +138,8 @@ def guess(coordinates, data): phi = -np.angle(fft_data[f_max_index], deg=True) # tau guess: pick the point where we reach roughly 1/e - one_over_e_val = of + A/3. - one_over_e_idx = np.argmin(np.abs(data-one_over_e_val)) + one_over_e_val = of + A / 3.0 + one_over_e_idx = np.argmin(np.abs(data - one_over_e_val)) tau = coordinates[one_over_e_idx] return dict(A=A, of=of, phi=phi, f=f, tau=tau) @@ -130,12 +147,17 @@ def guess(coordinates, data): class Gaussian(Fit): @staticmethod - def model(coordinates, x0, sigma, A, of): + def model( + coordinates: np.ndarray, x0: float, sigma: float, A: float, of: float + ) -> np.ndarray: """$A * np.exp(-(x-x_0)^2/(2\sigma^2)) + of""" - return A * np.exp(-(coordinates - x0) ** 2 / (2 * sigma ** 2)) + of + return A * np.exp(-((coordinates - x0) ** 2) / (2 * sigma**2)) + of @staticmethod - def guess(coordinates, data): + def guess( + coordinates: Union[Tuple[np.ndarray, ...], np.ndarray], data: np.ndarray + ) -> Dict[str, float]: + assert isinstance(coordinates, np.ndarray) # TODO: very crude at the moment, not likely to work well with not-so-nice data. of = np.mean(data) dev = data - of @@ -144,4 +166,3 @@ def guess(coordinates, data): A = data[i_max] - of sigma = np.abs((coordinates[-1] - coordinates[0])) / 20 return dict(x0=x0, sigma=sigma, A=A, of=of) - diff --git a/labcore/analysis/hv_pretty.py b/src/labcore/analysis/hv_pretty.py similarity index 61% rename from labcore/analysis/hv_pretty.py rename to src/labcore/analysis/hv_pretty.py index 8c5f3a2..a3dedff 100644 --- a/labcore/analysis/hv_pretty.py +++ b/src/labcore/analysis/hv_pretty.py @@ -1,18 +1,22 @@ -import seaborn as sns +from pathlib import Path +from typing import Any, Optional + import holoviews as hv import hvplot +import seaborn as sns from PIL import Image -from pathlib import Path -hv.extension('bokeh') +hv.extension("bokeh") + # Convert inches to pixels -def correctly_sized_figure(width=6, height=4): +def correctly_sized_figure(width: float = 6, height: float = 4) -> dict[str, int]: """Returns width and height in pixels from inches.""" - return {'width': int(width * 300), 'height': int(height * 300)} + return {"width": int(width * 300), "height": int(height * 300)} + # Set Arial font for all text elements -def set_arial_font(plot, element=None): +def set_arial_font(plot: Any, element: Any = None) -> None: """Applies Arial font to all textual elements of a bokeh-based hvplot.""" p = plot.state p.title.text_font = "Arial" @@ -29,49 +33,71 @@ def set_arial_font(plot, element=None): item.label_text_font = "Arial" item.label_text_font_style = "normal" + # Axis and label formatting -def format_ax(plot, title=None, xlabel=None, ylabel=None, - fontsize=12, title_fontsize=14, - xticks=None, yticks=None, xlim=None, ylim=None, - axes_pad=0.05, tick_fontsize=10): +def format_ax( + plot: Any, + title: Optional[str] = None, + xlabel: Optional[str] = None, + ylabel: Optional[str] = None, + fontsize: int = 12, + title_fontsize: int = 14, + xticks: Any = None, + yticks: Any = None, + xlim: Any = None, + ylim: Any = None, + axes_pad: float = 0.05, + tick_fontsize: int = 10, +) -> Any: """Apply axis and label formatting options to a hvplot object.""" opts_dict = { - 'title': title, - 'xlabel': xlabel, - 'ylabel': ylabel, - 'xlim': xlim, - 'ylim': ylim, - 'xticks': xticks, - 'yticks': yticks, - 'padding': axes_pad, - 'fontsize': { - 'title': f"{title_fontsize}pt", - 'labels': f"{fontsize}pt", - 'xticks': f"{tick_fontsize}pt", - 'yticks': f"{tick_fontsize}pt", - 'legend': f"{fontsize}pt" - } + "title": title, + "xlabel": xlabel, + "ylabel": ylabel, + "xlim": xlim, + "ylim": ylim, + "xticks": xticks, + "yticks": yticks, + "padding": axes_pad, + "fontsize": { + "title": f"{title_fontsize}pt", + "labels": f"{fontsize}pt", + "xticks": f"{tick_fontsize}pt", + "yticks": f"{tick_fontsize}pt", + "legend": f"{fontsize}pt", + }, } - + plot = plot.opts(**{k: v for k, v in opts_dict.items() if v is not None}) plot.opts(responsive=False) return plot + # Add legend to Overlay or Layouts -def add_legend(plot, location='top_right', show=True): +def add_legend(plot: Any, location: str = "top_right", show: bool = True) -> Any: """Configure legend visibility and position.""" if isinstance(plot, hv.Overlay) or isinstance(plot, hv.Layout): plot = plot.opts(show_legend=show, legend_position=location) return plot + # Setup seaborn style -def setup_plotting(style='whitegrid', context='notebook', font_scale=1.2): +def setup_plotting( + style: str = "whitegrid", context: str = "notebook", font_scale: float = 1.2 +) -> None: """Sets up seaborn styling globally for consistency with matplotlib-style aesthetics.""" sns.set_style(style) sns.set_context(context, font_scale=font_scale) -def save_plot_as_png(plot, filename, width_in=6, height_in=4, dpi=300, embed_dpi=True): +def save_plot_as_png( + plot: Any, + filename: Any, + width_in: float = 6, + height_in: float = 4, + dpi: int = 300, + embed_dpi: bool = True, +) -> None: """ Save a Holoviews plot to a high-resolution PNG with embedded DPI metadata. @@ -95,11 +121,11 @@ def save_plot_as_png(plot, filename, width_in=6, height_in=4, dpi=300, embed_dpi plot = plot.opts(width=width_px, height=height_px) # Save to temporary file using Holoviews - hvplot.save(plot, tmp_path, fmt='png') + hvplot.save(plot, tmp_path, fmt="png") if embed_dpi: img = Image.open(tmp_path) img.save(output_path, dpi=(dpi, dpi)) tmp_path.unlink() # Delete temp file else: - tmp_path.rename(output_path) \ No newline at end of file + tmp_path.rename(output_path) diff --git a/labcore/analysis/hvapps.py b/src/labcore/analysis/hvapps.py similarity index 76% rename from labcore/analysis/hvapps.py rename to src/labcore/analysis/hvapps.py index db2b58a..3a622bb 100644 --- a/labcore/analysis/hvapps.py +++ b/src/labcore/analysis/hvapps.py @@ -1,61 +1,59 @@ +import asyncio +import logging +import re +from collections import OrderedDict from datetime import datetime from pathlib import Path from typing import Any, Union -from collections import OrderedDict -import logging -import asyncio -import nest_asyncio -nest_asyncio.apply() - -import hvplot import holoviews as hv - -from bokeh.io.export import export_png +import hvplot +import nest_asyncio import pandas -import param import panel as pn -from panel.widgets import RadioButtonGroup as RBG, Select -from watchdog.observers import Observer +import param +from bokeh.io.export import export_png +from panel.widgets import RadioButtonGroup as RBG +from panel.widgets import Select from watchdog.events import FileSystemEventHandler +from watchdog.observers import Observer -import re - -from ..data.datadict_storage import find_data, timestamp_from_path, datadict_from_hdf5 from ..data.datadict import ( DataDict, - dd2df, datadict_to_meshgrid, + dd2df, dd2xr, ) +from ..data.datadict_storage import datadict_from_hdf5, find_data, timestamp_from_path from ..utils.misc import add_end_number_to_repeated_file -from .hvplotting import Node, labeled_widget, PlotNode +from .hvplotting import Node, PlotNode, labeled_widget + +nest_asyncio.apply() logger = logging.getLogger(__name__) class Handler(FileSystemEventHandler): - def __init__(self, update_callback): + def __init__(self, update_callback: Any) -> None: self.update_callback = update_callback - def on_created(self, event): + def on_created(self, event: Any) -> None: if not event.is_directory: # Get file extension and compare to data file type, ddh5 file_type = Path(event.src_path).suffix # TODO: Generalize to other data file types - if file_type == '.ddh5': + if file_type == ".ddh5": self.update_callback(event) class DataSelect(pn.viewable.Viewer): - SYM = { - 'complete': '✅', - 'star': '😁', - 'bad': '😭', - 'trash': '❌', + "complete": "✅", + "star": "😁", + "bad": "😭", + "trash": "❌", } - DATAFILE = 'data.ddh5' + DATAFILE = "data.ddh5" selected_path = param.Parameter(None) search_term = param.Parameter(None) @@ -65,16 +63,16 @@ class DataSelect(pn.viewable.Viewer): event_lock = False @staticmethod - def date2label(date_tuple): + def date2label(date_tuple: Any) -> str: return "-".join((str(k) for k in date_tuple)) @staticmethod - def label2date(label): - return tuple(int(l) for l in label[:10].split("-")) + def label2date(label: Any) -> Any: + return tuple(int(part) for part in label[:10].split("-")) @staticmethod - def group_data(data_list): - ret = {} + def group_data(data_list: Any) -> Any: + ret: dict[Any, Any] = {} for path, info in data_list.items(): ts = timestamp_from_path(path) date = (ts.year, ts.month, ts.day) @@ -83,10 +81,10 @@ def group_data(data_list): ret[date][path] = info return ret - def __panel__(self): + def __panel__(self) -> Any: return self.layout - def __init__(self, data_root, size=15): + def __init__(self, data_root: Any, size: int = 15) -> None: super().__init__() self.size = size @@ -102,15 +100,14 @@ def __init__(self, data_root, size=15): # a search bar for data self.text_input = pn.widgets.TextInput( - name='Search', - placeholder='Enter a search term here...' + name="Search", placeholder="Enter a search term here..." ) self.layout.append(self.text_input) # Display the current search term self.typed_value = pn.widgets.StaticText( stylesheets=[selector_stylesheet], - css_classes=['ttlabel'], + css_classes=["ttlabel"], ) self.layout.append(self.text_input_repeater) @@ -119,31 +116,27 @@ def __init__(self, data_root, size=15): # two selectors for data selection self._group_select_widget = pn.widgets.CheckBoxGroup( - name='Date', - width=200-self.feed_scroll_width, - stylesheets=[selector_stylesheet] + name="Date", + width=200 - self.feed_scroll_width, + stylesheets=[selector_stylesheet], ) # Wrap the CheckBoxGroup in a feed so that it can't get too long self._group_select_feed = pn.layout.Feed( - objects=[self._group_select_widget], - height=(self.size - 1) * 20, - width=200 + objects=[self._group_select_widget], height=(self.size - 1) * 20, width=200 ) # Add a title to match the multiselect widget style self._group_select = pn.Column( pn.widgets.StaticText( - stylesheets=[selector_stylesheet], - css_classes=['ttlabel'], - value="Date" + stylesheets=[selector_stylesheet], css_classes=["ttlabel"], value="Date" ), - self._group_select_feed + self._group_select_feed, ) # Data select panel self._data_select_widget = Select( - name='Data set', + name="Data set", size=self.size, width=800, - stylesheets=[selector_stylesheet] + stylesheets=[selector_stylesheet], ) # Scrollable feed of images stored with this data @@ -151,20 +144,26 @@ def __init__(self, data_root, size=15): # Data frame showing axes & dependencies self.data_info = pn.pane.DataFrame(None) - self.layout.append(pn.Row( - self._group_select, self.data_select, self.data_info, self.data_images_feed)) + self.layout.append( + pn.Row( + self._group_select, + self.data_select, + self.data_info, + self.data_images_feed, + ) + ) # a simple info panel about the selection self.lbl = pn.widgets.StaticText( stylesheets=[selector_stylesheet], - css_classes=['ttlabel'], + css_classes=["ttlabel"], ) self.layout.append(pn.Row(self.info_panel)) opts = OrderedDict() for k in sorted(self.data_sets.keys())[::-1]: - lbl = self.date2label(k) + f' [{len(self.data_sets[k])}]' + lbl = self.date2label(k) + f" [{len(self.data_sets[k])}]" opts[lbl] = k self._group_select_widget.options = opts @@ -175,18 +174,20 @@ def __init__(self, data_root, size=15): self.handler = Handler(self.update_group_options) self.start() - def start(self): - self.observer.schedule( - self.handler, self.DIRECTORY_TO_WATCH, recursive=True) + def start(self) -> None: + self.observer.schedule(self.handler, self.DIRECTORY_TO_WATCH, recursive=True) self.observer.start() @pn.depends("_group_select_widget.value") - def data_select(self): + def data_select(self) -> Any: # setup global variables for search function active_search = False r = re.compile(".*") if hasattr(self, "text_input"): - if self.text_input.value_input is not None and self.text_input.value_input != "": + if ( + self.text_input.value_input is not None + and self.text_input.value_input != "" + ): # Make the Regex expression for the searched string r = re.compile(".*" + str(self.text_input.value_input) + ".*") active_search = True @@ -196,13 +197,17 @@ def data_select(self): self._data_select_widget.options = opts return self._data_select_widget - def get_data_options(self, active_search=True, r=re.compile('.*')): + def get_data_options( + self, active_search: bool = True, r: Any = re.compile(".*") + ) -> Any: if isinstance(r, str): r = re.compile(r) opts = OrderedDict() for d in self._group_select_widget.value: for dset in sorted(self.data_sets[d].keys())[::-1]: - if active_search and not r.match(str(dset) + " " + str(timestamp_from_path(dset))): + if active_search and not r.match( + str(dset) + " " + str(timestamp_from_path(dset)) + ): # If Active search and this term doesn't match it, don't show continue (dirs, files) = self.data_sets[d][dset] @@ -212,14 +217,14 @@ def get_data_options(self, active_search=True, r=re.compile('.*')): name = f"{dset.stem[27:]}" date = f"{ts.date()}" lbl = f"{date} - {time} - {uuid} - {name} " - for k in ['complete', 'star', 'trash']: - if f'__{k}__.tag' in files: + for k in ["complete", "star", "trash"]: + if f"__{k}__.tag" in files: lbl += self.SYM[k] opts[lbl] = dset return opts @pn.depends("_data_select_widget.value") - def info_panel(self): + def info_panel(self) -> Any: path = self._data_select_widget.value # Setup data preview panel if path is not None: @@ -229,13 +234,15 @@ def info_panel(self): for file in Path.iterdir(abs_path): # Check if the file ends with png, jpg, or jpeg file = str(file) - img = '' + img = "" if file.endswith(".png"): - img = pn.pane.PNG(file, sizing_mode="fixed", - width=self.image_feed_width) + img = pn.pane.PNG( + file, sizing_mode="fixed", width=self.image_feed_width + ) elif file.endswith(".jpg") or file.endswith(".jpeg"): - img = pn.pane.JPG(file, sizing_mode="fixed", - width=self.image_feed_width) + img = pn.pane.JPG( + file, sizing_mode="fixed", width=self.image_feed_width + ) else: continue images.append(img) @@ -249,13 +256,14 @@ def info_panel(self): dict_for_dataframe = {} for key in dd.keys(): if len(key) < 2 or key[0:2] != "__": - depends_on = dd[key]["axes"] if dd[key]["axes"] != [ - ] else "Independent" - dict_for_dataframe[key] = [ - dd[key]["__shape__"], depends_on] + depends_on = ( + dd[key]["axes"] if dd[key]["axes"] != [] else "Independent" + ) + dict_for_dataframe[key] = [dd[key]["__shape__"], depends_on] # Convert to data frame and display df = pandas.DataFrame.from_dict( - data=dict_for_dataframe, orient="index", columns=['Shape', 'Depends on']) + data=dict_for_dataframe, orient="index", columns=["Shape", "Depends on"] + ) self.data_info.object = df # Get the path if isinstance(path, Path): @@ -266,18 +274,18 @@ def info_panel(self): return self.lbl @pn.depends("text_input.value_input") - def text_input_repeater(self): + def text_input_repeater(self) -> Any: self.typed_value.value = f"Current Search: {self.text_input.value_input}" self.search_term = self.text_input.value_input return self.typed_value - def update_group_options(self, event): + def update_group_options(self, event: Any) -> None: # Refresh self.data_sets new_data_set = self.group_data(find_data(root=self.data_root)) # Repull data group options new_opts = OrderedDict() for k in sorted(new_data_set.keys())[::-1]: - lbl = self.date2label(k) + f' [{len(new_data_set[k])}]' + lbl = self.date2label(k) + f" [{len(new_data_set[k])}]" new_opts[lbl] = k # Set the group and data options self.data_sets = new_data_set @@ -305,7 +313,7 @@ class LoaderNodeBase(Node): Each subclass must implement ``LoaderNodeBase.load_data``. """ - def __init__(self, path=Path('.'), *args: Any, **kwargs: Any): + def __init__(self, path: Any = Path("."), *args: Any, **kwargs: Any) -> None: """Constructor for ``LoaderNode``. Parameters @@ -322,30 +330,29 @@ def __init__(self, path=Path('.'), *args: Any, **kwargs: Any): self.refresh = pn.widgets.Select( name="Auto-refresh", options={ - 'None': None, - '2 s': 2, - '5 s': 5, - '10 s': 10, - '1 min': 60, - '10 min': 600, + "None": None, + "2 s": 2, + "5 s": 5, + "10 s": 10, + "1 min": 60, + "10 min": 600, }, value="None", width=80, ) - self.task = None + self.task: Any = None # Determines if save_buttons need to be disabled. Must be run early # to avoid an 'attribute does not exist' error. self.disable_buttons = not self.can_save() - super().__init__(path=path, *args, **kwargs) + super().__init__(path, *args, **kwargs) # Store whether or not each graph type can be saved as html/png. # Must be created before plot_col column self.graph_type_savable = {} for k in self.graph_types: - self.graph_type_savable[k] = hasattr( - self.graph_types[k], 'get_plot') + self.graph_type_savable[k] = hasattr(self.graph_types[k], "get_plot") self.pre_process_opts = RBG( options=[None, "Average"], @@ -389,7 +396,7 @@ def __init__(self, path=Path('.'), *args: Any, **kwargs: Any): self.plot_col = pn.Column(objects=self.plot) # The Leading pn.Row is used to make the fit box appear at right - self.layout = pn.Row( + self.layout = pn.Row( pn.Column( pn.Row( labeled_widget(self.pre_process_opts), @@ -401,15 +408,12 @@ def __init__(self, path=Path('.'), *args: Any, **kwargs: Any): self.png_button, ), self.display_info, - pn.Row( - self.buffer_col, - self.plot_col - ) + pn.Row(self.buffer_col, self.plot_col), ), self.fit_obj, ) - #Create var to collect the fitfunc inputs + # Create var to collect the fitfunc inputs self.fit_inputs = None self.lock = asyncio.Lock() @@ -434,10 +438,8 @@ async def load_and_preprocess(self, *events: param.parameterized.Event) -> None: if self.pre_process_dim_input.value in indep: if self.pre_process_opts.value == "Average": - data = self.mean( - data, self.pre_process_dim_input.value) - indep.pop(indep.index( - self.pre_process_dim_input.value)) + data = self.mean(data, self.pre_process_dim_input.value) + indep.pop(indep.index(self.pre_process_dim_input.value)) # when making gridded data, can do things slightly differently # TODO: what if gridding goes wrong? @@ -456,21 +458,21 @@ async def load_and_preprocess(self, *events: param.parameterized.Event) -> None: self.data_out = data t1 = datetime.now() - self.info_label.value = f"Loaded data at {t1.strftime('%Y-%m-%d %H:%M:%S')} (in {(t1-t0).microseconds*1e-3:.0f} ms)." + self.info_label.value = f"Loaded data at {t1.strftime('%Y-%m-%d %H:%M:%S')} (in {(t1 - t0).microseconds * 1e-3:.0f} ms)." # Select None so that save buttons disabled/enabled works # I have no clue why but there's a bug if this doesn't happen save_val = self.plot_type_select.value - self.plot_type_select.value = 'None' + self.plot_type_select.value = "None" self.plot_type_select.value = save_val @pn.depends("data_out", "plot_type_select.value") - def plot_obj(self): + def plot_obj(self) -> Any: ret = super().plot_obj() self.toggle_save_buttons() return ret - def can_save(self): + def can_save(self) -> bool: # Returns TRUE is the necessary packages for saving html and images are installed # Returns FALSE and prints a notice about the uninstalled packages otherwise @@ -487,16 +489,18 @@ def can_save(self): # Reset Plot object self._plot_obj = None - + if not has_packages: - logger.warning("SAVING IMAGES DISABLED: You have not installed the necessary packages to allow for the saving of " - "images. To allow this functionality, please install Selenium, PhantomJS, Firefox, and " - "Geckodriver") + logger.warning( + "SAVING IMAGES DISABLED: You have not installed the necessary packages to allow for the saving of " + "images. To allow this functionality, please install Selenium, PhantomJS, Firefox, and " + "Geckodriver" + ) has_packages = True return has_packages - def toggle_save_buttons(self): + def toggle_save_buttons(self) -> None: if (self.disable_buttons) or (self.file_path == ""): # If the packages aren't installed, keep button disabled # If no file is selected, keep button disabled @@ -506,49 +510,55 @@ def toggle_save_buttons(self): self.html_button.disabled = _disabled self.png_button.disabled = _disabled - def save_html(self, *events: param.parameterized.Event): + def save_html(self, *events: param.parameterized.Event) -> None: if isinstance(self._plot_obj, PlotNode): - file_name = add_end_number_to_repeated_file(self.file_path.parent / f"{self.file_path.parent.name}.html") + file_name = add_end_number_to_repeated_file( + self.file_path.parent / f"{self.file_path.parent.name}.html" + ) hvplot.save(self._plot_obj.get_plot(), file_name) - def save_png(self, *events: param.parameterized.Event): - def save_p(p, suffix=""): + def save_png(self, *events: param.parameterized.Event) -> None: + def save_p(p: Any, suffix: str = "") -> None: path = add_end_number_to_repeated_file( - self.file_path.parent.joinpath(f"{self.file_path.parent.name}{suffix}.png")) + self.file_path.parent.joinpath( + f"{self.file_path.parent.name}{suffix}.png" + ) + ) # FIXME: We need all of this because different plot types return different objects. # It might be worth unifying it and always returning the same object. # If p is a Panel HoloViews pane, extract the HoloViews object - if hasattr(p, 'object') and hasattr(p.object, 'traverse'): + if hasattr(p, "object") and hasattr(p.object, "traverse"): # This is a Panel HoloViews pane - extract the HoloViews object hv_obj = p.object - elif hasattr(p, 'traverse'): + elif hasattr(p, "traverse"): # This is already a HoloViews object hv_obj = p else: # Skip objects that are not HoloViews or Panel HoloViews panes - logger.warning(f"Skipping object of type {type(p)} - not a HoloViews object") + logger.warning( + f"Skipping object of type {type(p)} - not a HoloViews object" + ) return - + bokeh_plot = hv.render(hv_obj) export_png(bokeh_plot, filename=path) # TODO: What happens when this is not a Node? What should happen then? if isinstance(self._plot_obj, PlotNode): - plot = self._plot_obj.get_plot() if isinstance(plot, pn.Column): for i, p in enumerate(plot): - suffix = f"_plot{i+1}" if len(plot) > 1 else "" + suffix = f"_plot{i + 1}" if len(plot) > 1 else "" save_p(p, suffix) else: save_p(plot) @pn.depends("info_label.value") - def display_info(self): + def display_info(self) -> Any: return self.info_label @pn.depends("refresh.value", watch=True) - def on_refresh_changed(self): + def on_refresh_changed(self) -> None: if self.refresh.value is None: self.task = None @@ -556,7 +566,7 @@ def on_refresh_changed(self): if self.task is None: self.task = asyncio.ensure_future(self.run_auto_refresh()) - async def run_auto_refresh(self): + async def run_auto_refresh(self) -> None: while self.refresh.value is not None: await asyncio.sleep(self.refresh.value) asyncio.run(self.load_and_preprocess()) @@ -572,6 +582,7 @@ def load_data(self) -> DataDict: """ raise NotImplementedError + Label_stylesheet = """ :host { font-family: monospace; @@ -579,6 +590,7 @@ def load_data(self) -> DataDict: } """ + class DDH5LoaderNode(LoaderNodeBase): """A node that loads data from a specified file location. @@ -601,7 +613,7 @@ def __init__(self, path: Union[str, Path] = "", *args: Any, **kwargs: Any): super().__init__(path, *args, **kwargs) self.file_path = path - def load_data(self) -> DataDict: + def load_data(self) -> Any: """ Load data from the file location specified """ diff --git a/labcore/analysis/hvplotting.py b/src/labcore/analysis/hvplotting.py similarity index 77% rename from labcore/analysis/hvplotting.py rename to src/labcore/analysis/hvplotting.py index 2155d4b..4920e07 100644 --- a/labcore/analysis/hvplotting.py +++ b/src/labcore/analysis/hvplotting.py @@ -41,7 +41,7 @@ from ..data.datadict_storage import NumpyEncoder from ..data.tools import data_dims, split_complex from ..utils.misc import add_end_number_to_repeated_file -from .fit import Fit, FitResult, plot_ds_2d_with_fit +from .fit import Fit, plot_ds_2d_with_fit logger = logging.getLogger(__name__) @@ -98,7 +98,13 @@ class Node(pn.viewable.Viewer): def __panel__(self) -> pn.viewable.Viewable: return self.layout - def __init__(self, path=None, data_in: Optional[Data] = None, *args: Any, **kwargs: Any): + def __init__( + self, + path: Any = None, + data_in: Optional[Data] = None, + *args: Any, + **kwargs: Any, + ): """Constructor for ``Node``. Parameters @@ -117,10 +123,12 @@ def __init__(self, path=None, data_in: Optional[Data] = None, *args: Any, **kwar self.layout = pn.Column() # -- options for plotting - self.graph_types = {"None": None, - "Value": ValuePlot, - "Readout hist.": ComplexHist, - "Magnitude & Phase": MagnitudePhasePlot} + self.graph_types = { + "None": None, + "Value": ValuePlot, + "Readout hist.": ComplexHist, + "Magnitude & Phase": MagnitudePhasePlot, + } self.plot_type_select = RBG( options=list(self.graph_types.keys()), @@ -329,13 +337,12 @@ def data_out_view(self) -> DataDisplay: return self.render_data(self.data_out) @pn.depends("data_out") - def plot(self) -> pn.viewable.Viewable: + def plot(self) -> Any: """A reactive panel object that allows selecting a plot type, and shows the plot. Updates on change of ``data_out``. """ - return [labeled_widget(self.plot_type_select), - self.plot_obj] + return [labeled_widget(self.plot_type_select), self.plot_obj] @pn.depends("data_out", "plot_type_select.value") def plot_obj(self) -> Optional["Node"]: @@ -352,7 +359,8 @@ def plot_obj(self) -> Optional["Node"]: if self._plot_obj is not None: self.detach(self._plot_obj) self._plot_obj = ValuePlot( - name="plot", data_in=self.data_out, path=self.file_path) + name="plot", data_in=self.data_out, path=self.file_path + ) self.append(self._plot_obj) self._plot_obj.data_in = self.data_out @@ -361,7 +369,8 @@ def plot_obj(self) -> Optional["Node"]: if self._plot_obj is not None: self.detach(self._plot_obj) self._plot_obj = MagnitudePhasePlot( - name="plot", data_in=self.data_out, path=self.file_path) + name="plot", data_in=self.data_out, path=self.file_path + ) self.append(self._plot_obj) self._plot_obj.data_in = self.data_out @@ -370,7 +379,8 @@ def plot_obj(self) -> Optional["Node"]: if self._plot_obj is not None: self.detach(self._plot_obj) self._plot_obj = ComplexHist( - name="plot", data_in=self.data_out, path=self.file_path) + name="plot", data_in=self.data_out, path=self.file_path + ) self.append(self._plot_obj) self._plot_obj.data_in = self.data_out @@ -382,8 +392,7 @@ def plot_obj(self) -> Optional["Node"]: return self._plot_obj def append(self, other: "Node") -> None: - watcher = self.param.watch( - other.update, ["data_out", "units_out", "meta_out"]) + watcher = self.param.watch(other.update, ["data_out", "units_out", "meta_out"]) self._watchers[other] = watcher def detach(self, other: "Node") -> None: @@ -392,7 +401,7 @@ def detach(self, other: "Node") -> None: del self._watchers[other] @pn.depends("data_out", "plot_type_select.value") - def fit_obj(self): + def fit_obj(self) -> Any: # Returns the panel to select fit variables if isinstance(self._plot_obj, PlotNode): return self._plot_obj.get_fit_panel @@ -405,16 +414,16 @@ class ReduxNode(Node): coordinates = param.List(default=[]) operations = param.List(default=[]) - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self._widgets = {} + self._widgets: dict[str, Any] = {} self.layout = pn.Column() - def __panel__(self): + def __panel__(self) -> Any: return self.layout @pn.depends("data_in", watch=True) - def on_input_update(self): + def on_input_update(self) -> None: self.coords = list(self.data_in.dims.keys()) for c in self.coords: if c not in self._widgets: @@ -435,16 +444,15 @@ def on_input_update(self): self.on_widget_change() @pn.depends("operations") - def on_operations_change(self): + def on_operations_change(self) -> None: for c, o in zip(self.coords, self.operations): self._widgets[c].value = o - def on_widget_change(self, *events): - self.operations = [self._widgets[c] - ["widget"].value for c in self.coords] + def on_widget_change(self, *events: Any) -> None: + self.operations = [self._widgets[c]["widget"].value for c in self.coords] @pn.depends("data_in", "operations", watch=True) - def process(self): + def process(self) -> None: out = self.data_in for c, o in zip(self.coords, self.operations): if o == "Mean": @@ -460,7 +468,7 @@ class XYSelect(pn.viewable.Viewer): ] ) - def __init__(self): + def __init__(self) -> None: self._xrbg = RBG(options=self.options, name="x") self._yrbg = RBG(options=self.options, name="y") super().__init__() @@ -472,16 +480,16 @@ def __init__(self): self._sync_x() self._sync_y() - def __panel__(self): + def __panel__(self) -> Any: return self._layout @param.depends("options", watch=True) - def on_option_change(self): + def on_option_change(self) -> None: self._xrbg.options = self.options self._yrbg.options = self.options @param.depends("value", watch=True) - def _sync_widgets(self): + def _sync_widgets(self) -> None: if self.value[0] == self.value[1] and self.value[0] != "None": self.value = self.value[0], "None" self._xrbg.name = self.name @@ -489,7 +497,7 @@ def _sync_widgets(self): self._yrbg.value = self.value[1] @param.depends("_xrbg.value", watch=True) - def _sync_x(self): + def _sync_x(self) -> None: x = self._xrbg.value y = self.value[1] if y == x: @@ -497,7 +505,7 @@ def _sync_x(self): self.value = (x, y) @param.depends("_yrbg.value", watch=True) - def _sync_y(self): + def _sync_y(self) -> None: y = self._yrbg.value x = self.value[0] if y == x: @@ -507,18 +515,20 @@ def _sync_y(self): # -- generic plot functions + class PlotNode(Node): """PlotNode, Node subclass A superclass of all nodes that make plots of some sort. Mostly deals with define/creating fits for whatever graph subnode is instantiated. """ + refresh_graph = param.Parameter(None) - FITS = None + FITS: Optional[dict[str, Any]] = None @staticmethod - def load_fits_from_config(): + def load_fits_from_config() -> None: PlotNode.FITS = {} yaml = ruamel.yaml.YAML() cwd = Path.cwd() @@ -529,10 +539,10 @@ def load_fits_from_config(): ) raw_config = yaml.load(config_path) # Add fits to PlotNode.FITS if fits in the Config - if 'fits' in raw_config: - for ff in raw_config['fits']: + if "fits" in raw_config: + for ff in raw_config["fits"]: # Module and Class name from the string - modname = str(ff).rsplit('.', 1) + modname = str(ff).rsplit(".", 1) mod = modname[0] try: module = importlib.import_module(mod) @@ -543,89 +553,93 @@ def load_fits_from_config(): msg = f"Could not access Class {modname[1]} from module {modname[0]}. Exception: {e}" logger.error(msg) - def __init__(self, path=None, *args, **kwargs): + def __init__(self, path: Any = None, *args: Any, **kwargs: Any) -> None: self.path = path if PlotNode.FITS is None: self.load_fits_from_config() - self.fit_dict: dict = {} # Create fit_dict from json based on path - if path == '.' or path == '' or path is None: + if path == "." or path == "" or path is None: self.fit_data_path = None else: _dir = Path(path).parent self.fit_data_path = _dir.joinpath("fit_data.json") - self.fit_result = None - self.fit_dict = {} + self.fit_result: Any = None + self.fit_dict: dict[str, Any] = {} # Initialize _fit_axis_options to avoid errors when there's no data. # This should get properly set in the process() function - self._fit_axis_options = [] + self._fit_axis_options: list[Any] = [] # Initialize fit layout variables so they can be checked - self.fit_inputs = None - self.save_fit_button = None - self.reguess_fit_button = None - self.model_fit_button = None - self.refit_button = None - self.fit_box = None + self.fit_inputs: Any = None + self.save_fit_button: Any = None + self.reguess_fit_button: Any = None + self.model_fit_button: Any = None + self.refit_button: Any = None + self.fit_box: Any = None # A toggle variable to refresh the graph self.refresh_graph = True super().__init__(*args, **kwargs) + assert PlotNode.FITS is not None fit_options = list(PlotNode.FITS.keys()) - fit_options.append('None') + fit_options.append("None") self.fit_button = pn.widgets.MenuButton( - name="Fit", items=fit_options, button_type='success', width=100 + name="Fit", items=fit_options, button_type="success", width=100 ) self.fit_button.on_click(self.set_fit_box) self.select_fit_axis = pn.widgets.Select( - name='Fit Axis', - options=self._fit_axis_options if self._fit_axis_options is not None else [], + name="Fit Axis", + options=self._fit_axis_options + if self._fit_axis_options is not None + else [], ) - self.select_fit_axis.param.watch(self.set_fit_box, 'value') + self.select_fit_axis.param.watch(self.set_fit_box, "value") self.fit_layout = pn.Column( - pn.Row(self.fit_button, - self.select_fit_axis), + pn.Row(self.fit_button, self.select_fit_axis), ) - def get_fit_panel(self): + def get_fit_panel(self) -> Any: return self.fit_layout - - def process(self): + + def process(self) -> None: """Make a copy of the data so that changes (added fits) don't carry to other graphs/other analysis. Add saved arguments for all fits/axes.""" self.data_out = copy.copy(self.data_in) # Set fit_axis_options based on the function. Default to [] if returns None self._fit_axis_options = self.fit_axis_options() + assert PlotNode.FITS is not None # Draw any fits that already exist for axis in self.fit_axis_options(): if axis in self.fit_dict.keys(): - func_name = self.fit_dict[axis]['fit_function'] + func_name = self.fit_dict[axis]["fit_function"] saved_args = self.get_values(axis) if func_name not in PlotNode.FITS: msg = f"Axis {axis} has a fit of type {func_name} saved, which you don't have access to." print(msg) - pn.state.notifications.error(msg, duration=0) + pn.state.notifications.error(msg, duration=0) # type: ignore[union-attr] else: - self.update_dataset_by_fit_and_axis(PlotNode.FITS[func_name], saved_args, axis, True) + self.update_dataset_by_fit_and_axis( + PlotNode.FITS[func_name], saved_args, axis, True + ) - def plot_panel(self): + def plot_panel(self) -> Any: """Creates and returns a panel with the class's plot Should be overridden by subclasses to return the desired plot. """ return NotImplementedError - def get_plot(self): + def get_plot(self) -> Any: """Returns the plot as a holoviews object Used for saving the plot as html or png. @@ -635,28 +649,36 @@ def get_plot(self): return self.plot_panel() def fit_axis_options(self) -> list: - """Returns a list of the different axes you can + """Returns a list of the different axes you can make a fit for in this node. Should be overridden by subclasses to return the appropriate object. """ return [] - def set_fit_box(self, *events: param.parameterized.Event, fitted:bool=None): + def set_fit_box( + self, *events: param.parameterized.Event, fitted: Optional[bool] = None + ) -> None: if fitted is None: fitted = False if self.select_fit_axis.value in self.fit_dict.keys(): - if 'start_params' not in self.fit_dict[self.select_fit_axis.value].keys(): + if ( + "start_params" + not in self.fit_dict[self.select_fit_axis.value].keys() + ): fitted = True - # Delete start parameters when refreshing the fit box. Creating the + # Delete start parameters when refreshing the fit box. Creating the # fit box will regenerate these. if self.select_fit_axis.value in self.fit_dict.keys(): - if 'start_params' in self.fit_dict[self.select_fit_axis.value].keys(): - del self.fit_dict[self.select_fit_axis.value]['start_params'] - self.set_fit_box_helper(self.fit_button.clicked != - 'None', self.fit_button.clicked, fitted=fitted) + if "start_params" in self.fit_dict[self.select_fit_axis.value].keys(): + del self.fit_dict[self.select_fit_axis.value]["start_params"] + self.set_fit_box_helper( + self.fit_button.clicked != "None", self.fit_button.clicked, fitted=fitted + ) - def set_fit_box_helper(self, new_box: bool, fit_func_name: str, fitted: bool=False): + def set_fit_box_helper( + self, new_box: bool, fit_func_name: str, fitted: bool = False + ) -> None: """Removes and/or creates a fit box. If new_box == True this will (re)create the fit box.""" # Check if fit_box exists & get fit_box @@ -669,8 +691,7 @@ def set_fit_box_helper(self, new_box: bool, fit_func_name: str, fitted: bool=Fal if new_box: self.fit_box = self.add_fit_box(fit_func_name, fitted=fitted) - def remove_fit_box(self): - fit_box = self.fit_layout.objects[len(self.fit_layout.objects)-1] + def remove_fit_box(self) -> None: # Get all fit objects other than layout and set as the current objects no_fit_objects = self.fit_layout.objects[:-1] self.fit_layout.objects = no_fit_objects @@ -678,19 +699,22 @@ def remove_fit_box(self): self.save_fit_button = None self.fit_box = None - def add_fit_box(self, selected=None, fitted=False): + def add_fit_box(self, selected: Any = None, fitted: bool = False) -> Any: """Create a widget box for creating a fit.""" if selected is None: selected = self.fit_button.clicked if self.select_fit_axis.value in self.fit_dict.keys(): - if self.fit_dict[self.select_fit_axis.value]['fit_function'] != selected: + if self.fit_dict[self.select_fit_axis.value]["fit_function"] != selected: fitted = False # Create a widget box, add the name of the fit function at top - objs = [pn.widgets.StaticText( - name='FITTED' if fitted else 'Setup', - value=selected, - align="center", - )] + objs = [ + pn.widgets.StaticText( + name="FITTED" if fitted else "Setup", + value=selected, + align="center", + ) + ] + assert PlotNode.FITS is not None fit_class = PlotNode.FITS[selected] # Get guesses or saved values for all variables and make inputs saved_args = self.get_arguments() @@ -700,19 +724,28 @@ def add_fit_box(self, selected=None, fitted=False): continue name = var if fitted: - objs.append( pn.widgets.StaticText( - name=var, - value=str(saved_args[var]) + " +/- " + str(self.fit_dict[self.select_fit_axis.value]['params'][var]['stderr']), - align="start", - )) + objs.append( + pn.widgets.StaticText( + name=var, + value=str(saved_args[var]) + + " +/- " + + str( + self.fit_dict[self.select_fit_axis.value]["params"][var][ + "stderr" + ] + ), + align="start", + ) + ) else: - objs.append(pn.widgets.FloatInput( - name=name, - # Set value to the saved_args (or Ansatz) or to 0 - value=saved_args[var] if var in list(saved_args.keys()) else 0, + objs.append( + pn.widgets.FloatInput( + name=name, + # Set value to the saved_args (or Ansatz) or to 0 + value=saved_args[var] if var in list(saved_args.keys()) else 0, + ) ) - ) - objs[i].param.watch(self.update_fit_args, 'value') + objs[i].param.watch(self.update_fit_args, "value") # Add buttons to model the fit, reset the fit self.reguess_fit_button = pn.widgets.Button( name="Reguess", align="center", button_type="default", disabled=False @@ -739,29 +772,36 @@ def add_fit_box(self, selected=None, fitted=False): else: objs.append(pn.Row(self.model_fit_button, self.reguess_fit_button)) # Add to the layout - self.fit_inputs = pn.WidgetBox(name=selected, - objects=objs - ) + self.fit_inputs = pn.WidgetBox(name=selected, objects=objs) self.fit_layout.append(pn.Row(objects=[self.fit_inputs], name="fit_box")) # Save to fit_dict if self.select_fit_axis.value not in self.fit_dict.keys(): self.fit_dict[self.select_fit_axis.value] = { - 'fit_function': self.fit_button.clicked, 'start_params': saved_args, 'params': {}} + "fit_function": self.fit_button.clicked, + "start_params": saved_args, + "params": {}, + } # Resave to json for case of new fit class - elif 'start_params' not in self.fit_dict[self.select_fit_axis.value]: - self.fit_dict[self.select_fit_axis.value]['start_params'] = saved_args - self.fit_dict[self.select_fit_axis.value]['fit_function'] = self.fit_button.clicked + elif "start_params" not in self.fit_dict[self.select_fit_axis.value]: + self.fit_dict[self.select_fit_axis.value]["start_params"] = saved_args + self.fit_dict[self.select_fit_axis.value]["fit_function"] = ( + self.fit_button.clicked + ) self.update_fit_args(None) return self.fit_inputs - def save_fit(self, *events: param.parameterized.Event): + def save_fit(self, *events: param.parameterized.Event) -> None: """Saves only the currently selected fit axis to the json file""" params_dict = self.fit_result.params_to_dict() - params_path = add_end_number_to_repeated_file(self.path.parent.joinpath("fit_params.json")) + params_path = add_end_number_to_repeated_file( + self.path.parent.joinpath("fit_params.json") + ) fit_result = self.fit_result.lmfit_result.fit_report() - result_path = add_end_number_to_repeated_file(self.path.parent.joinpath("fit_result.txt")) + result_path = add_end_number_to_repeated_file( + self.path.parent.joinpath("fit_result.txt") + ) with open(params_path, "w") as outfile: json.dump(params_dict, outfile, cls=NumpyEncoder) @@ -769,22 +809,22 @@ def save_fit(self, *events: param.parameterized.Event): with open(result_path, "w") as outfile: outfile.write(fit_result) - def reguess_fit(self, event): + def reguess_fit(self, event: Any) -> None: """Resets the current args to the results of the Fit.guess() function for the current fit class.""" # Save the parameters from the current fit store_params = self.fit_dict[self.select_fit_axis.value] del self.fit_dict[self.select_fit_axis.value] # Redo the box (will add the axis back to fit_dict) - self.set_fit_box_helper( - True, store_params['fit_function']) + self.set_fit_box_helper(True, store_params["fit_function"]) # Restore parameters of current saved fit and refresh graph - self.fit_dict[self.select_fit_axis.value]['params'] = store_params['params'] + self.fit_dict[self.select_fit_axis.value]["params"] = store_params["params"] self.refresh_graph = True - def model_fit(self, *events: param.parameterized.Event): + def model_fit(self, *events: param.parameterized.Event) -> None: """Models the fit starting with the arguments already created""" # Get fit class, axis name, coordinate values + assert PlotNode.FITS is not None fit_class = PlotNode.FITS[self.fit_button.clicked] data_key = self.select_fit_axis.value np_data = [self.data_out[var].values for var in self.data_out.coords] @@ -795,33 +835,37 @@ def model_fit(self, *events: param.parameterized.Event): vals = self.data_out.data_vars[data_key].to_numpy() # Run the fit on the fit class fit = fit_class(coords, vals) - run_kwargs = self.fit_dict[self.select_fit_axis.value]['start_params'] + run_kwargs = self.fit_dict[self.select_fit_axis.value]["start_params"] self.fit_result = fit.run(**run_kwargs) # Get the Fit Result's arguments params_dict = self.fit_result.params_to_dict() fit_params = {} for k, v in params_dict.items(): - fit_params[k] = v['value'] + fit_params[k] = v["value"] # Update the dataset with the new data name = self.select_fit_axis.value self.update_dataset_by_fit_and_axis(fit_class, fit_params, name, saved=True) - self.fit_dict[self.select_fit_axis.value]['params'] = params_dict + self.fit_dict[self.select_fit_axis.value]["params"] = params_dict # switch to fitted fit_box - self.set_fit_box(None, fitted=True) + self.set_fit_box(None, fitted=True) self.refresh_graph = True - def get_arguments(self): + def get_arguments(self) -> Any: """Gets argument values for the currently selected fit and fit axis. Pulls data from the json if one exists, otherwise runs fit.guess and takes those values.""" axis = self.select_fit_axis.value fit_name = self.fit_button.clicked - if axis in self.fit_dict.keys() and self.fit_dict[axis]['fit_function'] == fit_name: + if ( + axis in self.fit_dict.keys() + and self.fit_dict[axis]["fit_function"] == fit_name + ): return self.get_values(axis) else: return self.get_ansatz() - def get_ansatz(self): + def get_ansatz(self) -> Any: + assert PlotNode.FITS is not None fit_class = PlotNode.FITS[self.fit_button.clicked] # Get the guess for this data and x # Set data_key to first data key & make numpy data @@ -834,27 +878,38 @@ def get_ansatz(self): coords = np_data[0:2] return fit_class.guess(coords, self.data_out.data_vars[data_key].to_numpy()) - def update_fit_args(self, event): + def update_fit_args(self, event: Any) -> None: """Updates the temporary saved value for all of the fit's starting arguments. - Called whenever a float input's value is changed, when the fitbox is - created, or when the fit_axis changes. """ + Called whenever a float input's value is changed, when the fitbox is + created, or when the fit_axis changes.""" if self.select_fit_axis.value not in self.fit_dict.keys(): self.fit_dict[self.select_fit_axis.value] = { - 'fit_function': self.fit_button.clicked, 'start_params': {}} + "fit_function": self.fit_button.clicked, + "start_params": {}, + } for i, obj in enumerate(self.fit_inputs.objects): if isinstance(obj, pn.widgets.FloatInput): - self.fit_dict[self.select_fit_axis.value]['start_params'][obj.name] = self.fit_inputs[i].value + self.fit_dict[self.select_fit_axis.value]["start_params"][obj.name] = ( + self.fit_inputs[i].value + ) + assert PlotNode.FITS is not None fit_class = PlotNode.FITS[self.fit_button.clicked] - params = self.fit_dict[self.select_fit_axis.value]['start_params'] - self.update_dataset_by_fit_and_axis(fit_class, params, self.select_fit_axis.value) + params = self.fit_dict[self.select_fit_axis.value]["start_params"] + self.update_dataset_by_fit_and_axis( + fit_class, params, self.select_fit_axis.value + ) self.refresh_graph = True - def update_dataset_by_fit_and_axis(self, fit_class: Fit, - model_args: dict[str, float], - model_axis_name: str, saved: bool = False): + def update_dataset_by_fit_and_axis( + self, + fit_class: Fit, + model_args: dict[str, float], + model_axis_name: str, + saved: bool = False, + ) -> None: """Updates the data for fit in the self.data_out dataset based on the given arguments.""" # Create np array of coordinates @@ -865,8 +920,8 @@ def update_dataset_by_fit_and_axis(self, fit_class: Fit, coords = np_data[0:2] # Model the data, name it, and add to self.data_out fit_data = fit_class.model(coords, **model_args) - fit_name = model_axis_name+"_fit" - fit_name_temp = model_axis_name+"_fit*" + fit_name = model_axis_name + "_fit" + fit_name_temp = model_axis_name + "_fit*" if not saved: # If not saved, deleted save data and add * to name if fit_name in self.data_out.keys(): @@ -877,20 +932,20 @@ def update_dataset_by_fit_and_axis(self, fit_class: Fit, if fit_name_temp in self.data_out.keys(): del self.data_out[fit_name_temp] self.update_dataset_by_data(fit_data, fit_name) - - def update_dataset_by_data(self, fit_data:np.ndarray, name:str): + + def update_dataset_by_data(self, fit_data: np.ndarray, name: str) -> None: # Get independent variable(s) and fit class indep, dep = self.data_dims(self.data_out) self.data_out[name] = (indep, fit_data) - def get_data_fit_names(self, axis_name, omit_axes=None): + def get_data_fit_names(self, axis_name: Any, omit_axes: Any = None) -> Any: if omit_axes is None: - omit_axes = ['Magnitude', 'Phase'] + omit_axes = ["Magnitude", "Phase"] # Check if a fit axis exists. Return list of axis and fit axis (if it exists) if isinstance(axis_name, list): # If given name is a list, loop through all names in list - ret = [] + ret: list[Any] = [] for name in axis_name: ret = ret + self.get_data_fit_names(name) return ret @@ -901,24 +956,24 @@ def get_data_fit_names(self, axis_name, omit_axes=None): # Check if a _fit or _fit* version of the name exists fit_name = axis_name + "_fit" ret = [axis_name] - if fit_name+"*" in self.data_out.data_vars.keys(): - ret.append(fit_name+"*") + if fit_name + "*" in self.data_out.data_vars.keys(): + ret.append(fit_name + "*") elif fit_name in self.data_out.data_vars.keys(): ret.append(fit_name) return ret - def get_values(self, axis:str): + def get_values(self, axis: str) -> Any: """Gets a dictionary of values from the result of the FitResult's params_to_dict() function.""" - if 'params' not in self.fit_dict[axis].keys(): - if 'start_params' in self.fit_dict[axis].keys(): - return self.fit_dict[axis]['start_params'] + if "params" not in self.fit_dict[axis].keys(): + if "start_params" in self.fit_dict[axis].keys(): + return self.fit_dict[axis]["start_params"] return [] - _dict = self.fit_dict[axis]['params'] + _dict = self.fit_dict[axis]["params"] values = {} for k in _dict.keys(): - values[k] = _dict[k]['value'] + values[k] = _dict[k]["value"] return values - + def indep_dims(self) -> int: indep, dep = self.data_dims(self.data_out) if isinstance(indep, list): @@ -929,9 +984,9 @@ def indep_dims(self) -> int: class ValuePlot(PlotNode): - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: self.xy_select = XYSelect() - self._old_indep = [] + self._old_indep: list[Any] = [] super().__init__(*args, **kwargs) @@ -940,11 +995,11 @@ def __init__(self, *args, **kwargs): self.plot_panel, ) - def __panel__(self): + def __panel__(self) -> Any: return self.layout @pn.depends("data_out") - def plot_options_panel(self): + def plot_options_panel(self) -> Any: indep, dep = self.data_dims(self.data_out) opts = ["None"] @@ -962,7 +1017,7 @@ def plot_options_panel(self): return self.xy_select @pn.depends("data_out", "xy_select.value", "refresh_graph") - def plot_panel(self): + def plot_panel(self) -> Any: self.refresh_graph = False plot = "*No valid options chosen.*" @@ -975,7 +1030,8 @@ def plot_panel(self): elif y in ["None", None]: if isinstance(self.data_out, pd.DataFrame): plot = self.data_out.hvplot.line( - x=x, xlabel=self.dim_label(x), + x=x, + xlabel=self.dim_label(x), y=self.get_data_fit_names(self.fit_axis_options()), ) * self.data_out.hvplot.scatter(x=x) @@ -991,31 +1047,37 @@ def plot_panel(self): # case: if x and y are selected, we make a 2d plot of some sort else: if isinstance(self.data_out, pd.DataFrame): - plot = plot_df_as_2d(self.data_out, x, y, - dim_labels=self.dim_labels(), - graph_axes=self.get_data_fit_names(self.fit_axis_options()) - ) + plot = plot_df_as_2d( + self.data_out, + x, + y, + dim_labels=self.dim_labels(), + graph_axes=self.get_data_fit_names(self.fit_axis_options()), + ) elif isinstance(self.data_out, xr.Dataset): - plot = plot_xr_as_2d(self.data_out, x, y, - dim_labels=self.dim_labels(), - graph_axes=self.get_data_fit_names(self.fit_axis_options()) - ) + plot = plot_xr_as_2d( + self.data_out, + x, + y, + dim_labels=self.dim_labels(), + graph_axes=self.get_data_fit_names(self.fit_axis_options()), + ) else: raise NotImplementedError plot = plot.cols(2) return plot - def fit_axis_options(self): + def fit_axis_options(self) -> Any: indep, dep = self.data_dims(self.data_out) ret = [] for d in dep: if d[-4:] != "_fit" and d[-5:] != "_fit*": ret.append(d) return list(dep) - + class ComplexHist(PlotNode): - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: self.gb_select = pn.widgets.CheckButtonGroup( name="Group by", options=[], @@ -1028,30 +1090,32 @@ def __init__(self, *args, **kwargs): self.plot_panel, ) - def __panel__(self): + def __panel__(self) -> Any: return self.layout @pn.depends("data_out", watch=True) - def _sync_options(self): + def _sync_options(self) -> None: indep, dep = self.data_dims(self.data_out) if isinstance(indep, list): self.gb_select.options = indep @pn.depends("data_out", "gb_select.value", "refresh_graph") - def plot_panel(self): + def plot_panel(self) -> Any: self.refresh_graph = False - t0 = time.perf_counter() + time.perf_counter() plot = "*No valid options chosen.*" layout = pn.Column() for k, v in self.complex_dependents(self.data_out).items(): - xlim = float(self.data_out[v["real"]].min()), float( - self.data_out[v["real"]].max() + xlim = ( + float(self.data_out[v["real"]].min()), + float(self.data_out[v["real"]].max()), ) - ylim = float(self.data_out[v["imag"]].min()), float( - self.data_out[v["imag"]].max() + ylim = ( + float(self.data_out[v["imag"]].min()), + float(self.data_out[v["imag"]].max()), ) p = self.data_out.hvplot( kind="hexbin", @@ -1068,21 +1132,20 @@ def plot_panel(self): return plot - def get_plot(self): + def get_plot(self) -> Any: plt = self.plot_panel() return plt[0].object - def fit_axis_options(self): - _dict = self.complex_dependents(self.data_out).items() - if not isinstance(_dict, dict): - _dict = dict(_dict) + def fit_axis_options(self) -> Any: + _items = self.complex_dependents(self.data_out).items() + _dict = dict(_items) return list(_dict.keys()) class MagnitudePhasePlot(PlotNode): - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: self.xy_select = XYSelect() - self._old_indep = [] + self._old_indep: list[Any] = [] super().__init__(*args, **kwargs) @@ -1091,9 +1154,10 @@ def __init__(self, *args, **kwargs): self.plot_panel, ) - def process(self): - assert isinstance( - self.data_in, xr.Dataset), "MagnitudePhasePlot needs an xr.Dataset, did not receive one." + def process(self) -> None: + assert isinstance(self.data_in, xr.Dataset), ( + "MagnitudePhasePlot needs an xr.Dataset, did not receive one." + ) # Convert the current dataset to one that has Magnitude and Phase columns indep, dep = self.data_dims(self.data_in) # Assign labels. This assumes the first column is the real coefficients. @@ -1105,14 +1169,14 @@ def process(self): phase = np.arctan(imaginary / real) super().process() # Add magnitude and phase data (don't effect self.data_in) - self.data_out['Magnitude'] = (indep, magnitude) - self.data_out['Phase'] = (indep, phase) + self.data_out["Magnitude"] = (indep, magnitude) + self.data_out["Phase"] = (indep, phase) - def __panel__(self): + def __panel__(self) -> Any: return self.layout @pn.depends("data_out") - def plot_options_panel(self): + def plot_options_panel(self) -> Any: indep, dep = self.data_dims(self.data_out) opts = ["None"] @@ -1130,7 +1194,7 @@ def plot_options_panel(self): return self.xy_select @pn.depends("data_out", "xy_select.value", "refresh_graph") - def plot_panel(self): + def plot_panel(self) -> Any: self.refresh_graph = False plot = "*No valid options chosen.*" @@ -1161,21 +1225,27 @@ def plot_panel(self): # case: if x and y are selected, we make a 2d plot of some sort else: - plot = plot_xr_as_2d(self.data_out, x, y, - dim_labels=self.dim_labels(), - graph_axes=self.get_data_fit_names(self.fit_axis_options())) + plot = plot_xr_as_2d( + self.data_out, + x, + y, + dim_labels=self.dim_labels(), + graph_axes=self.get_data_fit_names(self.fit_axis_options()), + ) plot = plot.cols(2) return plot - def fit_axis_options(self): - return ['Magnitude', 'Phase'] + def fit_axis_options(self) -> Any: + return ["Magnitude", "Phase"] - def get_data_fit_names(self, axis_name): + def get_data_fit_names(self, axis_name: Any, omit_axes: Any = None) -> Any: return super().get_data_fit_names(axis_name, []) -def plot_df_as_2d(df, x, y, dim_labels=None, graph_axes=None): +def plot_df_as_2d( + df: Any, x: Any, y: Any, dim_labels: Any = None, graph_axes: Any = None +) -> Any: if graph_axes is None: graph_axes = [] if dim_labels is None: @@ -1212,7 +1282,9 @@ def plot_df_as_2d(df, x, y, dim_labels=None, graph_axes=None): return "*that's currently not supported :(*" -def plot_xr_as_2d(ds, x, y, dim_labels=None, graph_axes=None): +def plot_xr_as_2d( + ds: Any, x: Any, y: Any, dim_labels: Any = None, graph_axes: Any = None +) -> Any: if graph_axes is None: graph_axes = [] @@ -1223,13 +1295,13 @@ def plot_xr_as_2d(ds, x, y, dim_labels=None, graph_axes=None): return "Nothing to plot." indeps, deps = Node.data_dims(ds) - plot = None + plot: Any = None # Set deps to the passed axes so it graphs all desired data if graph_axes: deps = graph_axes - if x + '_fit' in ds: + if x + "_fit" in ds: return plot_ds_2d_with_fit(ds, dim_labels.get(x, x), x, y) # plotting stuff vs two independent -- heatmap if x in indeps and y in indeps: @@ -1266,7 +1338,7 @@ def plot_xr_as_2d(ds, x, y, dim_labels=None, graph_axes=None): # -- various tool functions -def labeled_widget(w, lbl=None): +def labeled_widget(w: Any, lbl: Any = None) -> Any: m = w.margin if lbl is None: @@ -1288,4 +1360,4 @@ def plot_data(data: Union[pd.DataFrame, xr.Dataset]) -> pn.viewable.Viewable: return pn.Column( n, n.plot, - ) \ No newline at end of file + ) diff --git a/labcore/analysis/mpl.py b/src/labcore/analysis/mpl.py similarity index 86% rename from labcore/analysis/mpl.py rename to src/labcore/analysis/mpl.py index 3f323fe..8be81bd 100644 --- a/labcore/analysis/mpl.py +++ b/src/labcore/analysis/mpl.py @@ -1,31 +1,28 @@ -from typing import Tuple, List, Optional, Union, Any, Dict import logging - -import numpy as np -from numpy import ndarray -from numpy import complexfloating +from typing import Any, Dict, List, Optional, Tuple, Type, Union import matplotlib as mpl +import numpy as np +import seaborn as sns +import xarray as xr +from matplotlib import cm, gridspec, ticker from matplotlib import pyplot as plt -from matplotlib.figure import Figure from matplotlib.axes import Axes -from matplotlib import gridspec, cm, colors, ticker -from matplotlib.colors import rgb2hex -import seaborn as sns - -from .fit import FitResult, Fit, fit_and_add_to_ds +from matplotlib.colors import Colormap, rgb2hex +from matplotlib.figure import Figure +from .fit import Fit, FitResult, fit_and_add_to_ds logger = logging.getLogger(__name__) def fit_and_plot_1d( - ds, - name, - fit_class, + ds: xr.Dataset, + name: str, + fit_class: Type[Fit], dim_order: Optional[List[int]] = None, run_kwargs: Dict[str, Any] = {}, -): +) -> Tuple[xr.Dataset, FitResult, Figure]: ds2, result = fit_and_add_to_ds( ds=ds, dim_name=name, @@ -36,7 +33,7 @@ def fit_and_plot_1d( return ds2, result, plot_fit_1d(ds, name) -def plot_fit_1d(ds, name): +def plot_fit_1d(ds: xr.Dataset, name: str) -> Figure: datada = ds[name] fitda = ds[name + "_fit"] @@ -59,7 +56,7 @@ def plot_fit_1d(ds, name): format_ax( ax["res"], xlabel=f"{dimda.name} ({dimda.attrs.get('units', '')})", - ylabel=f"residuals", + ylabel="residuals", ) return fig @@ -268,25 +265,30 @@ def plot_fit_1d(ds, name): # color management tools -def get_color_cycle(n, colormap, start=0.0, stop=1.0, format="hex"): - if type(colormap) == str: - colormap = getattr(cm, colormap) +def get_color_cycle( + n: int, + colormap: Union[str, Colormap], + start: float = 0.0, + stop: float = 1.0, + format: str = "hex", +) -> List[str]: + cmap: Colormap = getattr(cm, colormap) if isinstance(colormap, str) else colormap pts = np.linspace(start, stop, n) if format == "hex": - colors = [rgb2hex(colormap(pt)) for pt in pts] + colors = [rgb2hex(cmap(pt)) for pt in pts] return colors # tools for color plots -def centers2edges(arr): +def centers2edges(arr: np.ndarray) -> np.ndarray: e = (arr[1:] + arr[:-1]) / 2.0 e = np.concatenate(([arr[0] - (e[0] - arr[0])], e)) e = np.concatenate((e, [arr[-1] + (arr[-1] - e[-1])])) return e -def pcolorgrid(xaxis, yaxis): +def pcolorgrid(xaxis: np.ndarray, yaxis: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: xedges = centers2edges(xaxis) yedges = centers2edges(yaxis) xx, yy = np.meshgrid(xedges, yedges) @@ -295,8 +297,13 @@ def pcolorgrid(xaxis, yaxis): # creating and formatting figures def correctly_sized_figure( - widths, heights, margins=0.5, dw=0.2, dh=0.2, make_axes=True -): + widths: List[float], + heights: List[float], + margins: Union[float, List[float]] = 0.5, + dw: float = 0.2, + dh: float = 0.2, + make_axes: bool = True, +) -> Any: """ Create a figure and grid where all dimensions are specified in inches. Arguments: @@ -312,18 +319,18 @@ def correctly_sized_figure( hsum = sum(heights) nrows = len(heights) ncols = len(widths) - if type(margins) == list: - l, r, t, b = margins + if isinstance(margins, list): + lm, r, t, b = margins else: - l = r = t = b = margins + lm = r = t = b = margins - figw = wsum + (ncols - 1) * dw + l + r + figw = wsum + (ncols - 1) * dw + lm + r figh = hsum + (nrows - 1) * dh + t + b # margins in fraction of the figure top = 1.0 - t / figh bottom = b / figh - left = l / figw + left = lm / figw right = 1.0 - r / figw # subplot spacing in fraction of the subplot size @@ -349,18 +356,18 @@ def correctly_sized_figure( def format_ax( - ax, - top=False, - right=False, - xlog=False, - ylog=False, - xlabel=None, - ylabel=None, - xlim=None, - ylim=None, - xticks=3, - yticks=3, -): + ax: Axes, + top: bool = False, + right: bool = False, + xlog: bool = False, + ylog: bool = False, + xlabel: Optional[str] = None, + ylabel: Optional[str] = None, + xlim: Optional[Tuple[float, float]] = None, + ylim: Optional[Tuple[float, float]] = None, + xticks: Union[int, List[float]] = 3, + yticks: Union[int, List[float]] = 3, +) -> None: ax.tick_params( axis="x", which="both", @@ -414,21 +421,24 @@ def format_ax( ax.yaxis.labelpad = 2 -def format_right_cb(cb): +def format_right_cb(cb: Any) -> None: cb.outline.set_visible(False) cb.ax.xaxis.set_visible(True) cb.ax.xaxis.set_label_position("top") def add_legend( - ax, anchor_point=(1, 1), legend_ref_point="lower right", **labels_and_handles -): + ax: Axes, + anchor_point: Tuple[float, float] = (1, 1), + legend_ref_point: str = "lower right", + **labels_and_handles: Any, +) -> None: if len(labels_and_handles) > 0: handles = [] labels = [] - for l, h in labels_and_handles.items(): + for lbl, h in labels_and_handles.items(): handles.append(h) - labels.append(l) + labels.append(lbl) ax.legend( handles, labels, @@ -440,7 +450,7 @@ def add_legend( ax.legend(bbox_to_anchor=anchor_point, borderpad=0, loc=legend_ref_point) -def setup_plotting(sns_style="whitegrid", rcparams={}): +def setup_plotting(sns_style: str = "whitegrid", rcparams: Dict[str, Any] = {}) -> None: # some sensible defaults for sizing, those are for a typical print-plot sns.set_style(sns_style) diff --git a/labcore/data/__init__.py b/src/labcore/data/__init__.py similarity index 100% rename from labcore/data/__init__.py rename to src/labcore/data/__init__.py diff --git a/labcore/data/datadict.py b/src/labcore/data/datadict.py similarity index 80% rename from labcore/data/datadict.py rename to src/labcore/data/datadict.py index 4aa19bc..c65aba3 100644 --- a/labcore/data/datadict.py +++ b/src/labcore/data/datadict.py @@ -3,22 +3,33 @@ Data classes we use throughout the plottr package, and tools to work on them. """ -import warnings + import copy as cp -import re import logging -import pandas as pd -import xarray as xr -import numpy as np +import re +import warnings from functools import reduce -from enum import Enum -from typing import List, Tuple, Dict, Sequence, Union, Any, Iterator, Optional, TypeVar +from typing import ( + Any, + Dict, + Iterator, + List, + Optional, + Sequence, + Tuple, + Type, + TypeVar, + Union, +) -from labcore.utils import num, misc +import numpy as np +import pandas as pd +import xarray as xr +from labcore.utils import misc, num -__author__ = 'Wolfgang Pfaff' -__license__ = 'MIT' +__author__ = "Wolfgang Pfaff" +__license__ = "MIT" logger = logging.getLogger(__name__) @@ -35,7 +46,7 @@ def is_meta_key(key: str) -> bool: :param key: The ``key`` we are checking. :return: ``True`` if it is, ``False`` if it isn't. """ - if key[:2] == '__' and key[-2:] == '__': + if key[:2] == "__" and key[-2:] == "__": return True else: return False @@ -56,7 +67,7 @@ def meta_key_to_name(key: str) -> str: if is_meta_key(key): return key[2:-2] else: - raise ValueError(f'{key} is not a meta key.') + raise ValueError(f"{key} is not a meta key.") def meta_name_to_key(name: str) -> str: @@ -66,10 +77,10 @@ def meta_name_to_key(name: str) -> str: :param name: The name that is being converted. :return: The meta data key based on ``name``. """ - return '__' + name + '__' + return "__" + name + "__" -T = TypeVar('T', bound='DataDictBase') +T = TypeVar("T", bound="DataDictBase") class GriddingError(ValueError): @@ -86,10 +97,13 @@ class DataDictBase(dict): def __init__(self, **kw: Any): super().__init__(self, **kw) - self.d_ = DataDictBase._DataAccess(self) + self.d_ = DataDictBase._DataAccess(self) def __eq__(self, other: object) -> bool: """Check for content equality of two datadicts.""" + # TODO: __ne__ is not overridden, so `!=` falls back to dict.__ne__ which + # tries to compare numpy array values directly and raises ValueError. + # Override __ne__ to return `not self.__eq__(other)`. if not isinstance(other, DataDictBase): return False else: @@ -135,7 +149,7 @@ def to_records(**data: Any) -> Dict[str, np.ndarray]: records: Dict[str, np.ndarray] = {} seqtypes = (np.ndarray, tuple, list) - nantypes = (type(None), ) + nantypes = (type(None),) for k, v in data.items(): if isinstance(v, seqtypes): @@ -182,8 +196,9 @@ def data_items(self) -> Iterator[Tuple[str, Dict[str, Any]]]: if not self._is_meta_key(k): yield k, v - def meta_items(self, data: Union[str, None] = None, - clean_keys: bool = True) -> Iterator[Tuple[str, Dict[str, Any]]]: + def meta_items( + self, data: Union[str, None] = None, clean_keys: bool = True + ) -> Iterator[Tuple[str, Dict[str, Any]]]: """ Generator for meta items. @@ -226,7 +241,7 @@ def data_vals(self, key: str) -> np.ndarray: """ if self._is_meta_key(key): raise ValueError(f"{key} is a meta key.") - return self[key].get('values', np.array([])) + return self[key].get("values", np.array([])) def has_meta(self, key: str) -> bool: """Check whether meta field exists in the dataset. @@ -314,8 +329,13 @@ def clear_meta(self, data: Union[str, None] = None) -> None: for m in data_meta_list: self.delete_meta(m, data) - def extract(self: T, data: List[str], include_meta: bool = True, - copy: bool = True, sanitize: bool = True) -> T: + def extract( + self: T, + data: List[str], + include_meta: bool = True, + copy: bool = True, + sanitize: bool = True, + ) -> T: """ Extract data from a dataset. @@ -366,8 +386,7 @@ def extract(self: T, data: List[str], include_meta: bool = True, # info about structure @staticmethod - def same_structure(*data: T, - check_shape: bool = False) -> bool: + def same_structure(*data: T, check_shape: bool = False) -> bool: """ Check if all supplied DataDicts share the same data structure (i.e., dependents and axes). @@ -383,10 +402,12 @@ def same_structure(*data: T, return True def empty_structure(d: T) -> T: - s = misc.unwrap_optional(d.structure(include_meta=False, add_shape=check_shape)) + s = misc.unwrap_optional( + d.structure(include_meta=False, add_shape=check_shape) + ) for k, v in s.data_items(): - if 'values' in v: - del s[k]['values'] + if "values" in v: + del s[k]["values"] return s s0 = empty_structure(data[0]) @@ -398,10 +419,13 @@ def empty_structure(d: T) -> T: return True - def structure(self: T, add_shape: bool = False, - include_meta: bool = True, - same_type: bool = False, - remove_data: Optional[List[str]] = None) -> Optional[T]: + def structure( + self: T, + add_shape: bool = False, + include_meta: bool = True, + same_type: bool = False, + remove_data: Optional[List[str]] = None, + ) -> Optional[T]: """ Get the structure of the DataDict. @@ -420,8 +444,9 @@ def structure(self: T, add_shape: bool = False, """ if add_shape: - warnings.warn("'add_shape' is deprecated and will be ignored", - DeprecationWarning) + warnings.warn( + "'add_shape' is deprecated and will be ignored", DeprecationWarning + ) add_shape = False if remove_data is None: @@ -432,13 +457,13 @@ def structure(self: T, add_shape: bool = False, for n, v in self.data_items(): if n not in remove_data: v2 = v.copy() - v2['values'] = [] + v2["values"] = [] s[n] = cp.deepcopy(v2) - if 'axes' in s[n]: + if "axes" in s[n]: for r in remove_data: - if r in s[n]['axes']: - i = s[n]['axes'].index(r) - s[n]['axes'].pop(i) + if r in s[n]["axes"]: + i = s[n]["axes"].index(r) + s[n]["axes"].pop(i) if include_meta: for n, v in self.meta_items(): @@ -451,24 +476,26 @@ def structure(self: T, add_shape: bool = False, return s return None - - def nbytes(self, name: Optional[str]=None) -> Optional[int]: + def nbytes(self, name: Optional[str] = None) -> Optional[int]: """Get the size of data. - - :param name: Name of the data field. if none, return size of + + :param name: Name of the data field. if none, return size of entire datadict. :return: size in bytes. """ if self.validate(): if name is None: - return sum([v['values'].size * v['values'].itemsize - for _, v in self.data_items()]) + return sum( + [ + v["values"].size * v["values"].itemsize + for _, v in self.data_items() + ] + ) else: return self.data_vals(name).size * self.data_vals(name).itemsize - - return None + return None def label(self, name: str) -> Optional[str]: """ @@ -482,14 +509,14 @@ def label(self, name: str) -> Optional[str]: if self.validate(): if name not in self: raise ValueError("No field '{}' present.".format(name)) - - if self[name]['label'] != '': - n = self[name]['label'] + + if self[name]["label"] != "": + n = self[name]["label"] else: n = name - if self[name]['unit'] != '': - n += ' ({})'.format(self[name]['unit']) + if self[name]["unit"] != "": + n += " ({})".format(self[name]["unit"]) return n return None @@ -522,9 +549,9 @@ def axes(self, data: Union[Sequence[str], str, None] = None) -> List[str]: lst = [] if data is None: for k, v in self.data_items(): - if 'axes' in v: - for n in v['axes']: - if n not in lst and self[n].get('axes', []) == []: + if "axes" in v: + for n in v["axes"]: + if n not in lst and self[n].get("axes", []) == []: lst.append(n) else: if isinstance(data, str): @@ -532,10 +559,10 @@ def axes(self, data: Union[Sequence[str], str, None] = None) -> List[str]: else: dataseq = data for n in dataseq: - if 'axes' not in self[n]: + if "axes" not in self[n]: continue - for m in self[n]['axes']: - if m not in lst and self[m].get('axes', []) == []: + for m in self[n]["axes"]: + if m not in lst and self[m].get("axes", []) == []: lst.append(m) return lst @@ -548,7 +575,7 @@ def dependents(self) -> List[str]: """ ret = [] for n, v in self.data_items(): - if len(v.get('axes', [])) != 0: + if len(v.get("axes", [])) != 0: ret.append(n) return ret @@ -586,34 +613,35 @@ def validate(self) -> bool: """ self._update_data_access() - msg = '\n' + msg = "\n" for n, v in self.data_items(): - - if 'axes' in v: - for na in v['axes']: + if "axes" in v: + for na in v["axes"]: if na not in self: - msg += " * '{}' has axis '{}', but no field " \ - "with name '{}' registered.\n".format( - n, na, na) + msg += ( + " * '{}' has axis '{}', but no field " + "with name '{}' registered.\n".format(n, na, na) + ) elif na not in self.axes(): - msg += " * '{}' has axis '{}', but no independent " \ - "with name '{}' registered.\n".format( - n, na, na) + msg += ( + " * '{}' has axis '{}', but no independent " + "with name '{}' registered.\n".format(n, na, na) + ) else: - v['axes'] = [] + v["axes"] = [] - if 'unit' not in v: - v['unit'] = '' + if "unit" not in v: + v["unit"] = "" - if 'label' not in v: - v['label'] = '' + if "label" not in v: + v["label"] = "" - vals = v.get('values', []) + vals = v.get("values", []) if type(vals) not in [np.ndarray, np.ma.core.MaskedArray]: vals = np.array(vals) - v['values'] = vals + v["values"] = vals - if msg != '\n': + if msg != "\n": raise ValueError(msg) return True @@ -631,7 +659,7 @@ def remove_unused_axes(self: T) -> T: used = False if n not in dependents: for m in dependents: - if n in self[m]['axes']: + if n in self[m]["axes"]: used = True else: used = True @@ -654,8 +682,9 @@ def sanitize(self: T) -> T: # axes order tools - def reorder_axes_indices(self, name: str, - **pos: int) -> Tuple[Tuple[int, ...], List[str]]: + def reorder_axes_indices( + self, name: str, **pos: int + ) -> Tuple[Tuple[int, ...], List[str]]: """ Get the indices that can reorder axes in a given way. @@ -670,8 +699,9 @@ def reorder_axes_indices(self, name: str, order = misc.reorder_indices_from_new_positions(axlist, **pos) return order, [axlist[i] for i in order] - def reorder_axes(self: T, data_names: Union[str, Sequence[str], None] = None, - **pos: int) -> T: + def reorder_axes( + self: T, data_names: Union[str, Sequence[str], None] = None, **pos: int + ) -> T: """ Reorder data axes. @@ -689,7 +719,7 @@ def reorder_axes(self: T, data_names: Union[str, Sequence[str], None] = None, for n in data_names: neworder, newaxes = self.reorder_axes_indices(n, **pos) - self[n]['axes'] = newaxes + self[n]["axes"] = newaxes self.validate() return self @@ -700,12 +730,12 @@ def copy(self: T) -> T: :return: A copy of the dataset. """ - logger.debug(f'copying a dataset with size {self.nbytes()}') + logger.debug(f"copying a dataset with size {self.nbytes()}") ret = self.structure() assert ret is not None for k, v in self.data_items(): - ret[k]['values'] = self.data_vals(k).copy() + ret[k]["values"] = self.data_vals(k).copy() return ret def astype(self: T, dtype: np.dtype) -> T: @@ -716,10 +746,10 @@ def astype(self: T, dtype: np.dtype) -> T: :return: Dataset, with values as given type (not a copy) """ for k, v in self.data_items(): - vals = v['values'] - if type(v['values']) not in [np.ndarray, np.ma.core.MaskedArray]: - vals = np.array(v['values']) - self[k]['values'] = vals.astype(dtype) + vals = v["values"] + if type(v["values"]) not in [np.ndarray, np.ma.core.MaskedArray]: + vals = np.array(v["values"]) + self[k]["values"] = vals.astype(dtype) return self @@ -735,16 +765,16 @@ def mask_invalid(self: T) -> T: vals.fill_value = np.nan except TypeError: vals.fill_value = -9999 - self[d]['values'] = vals + self[d]["values"] = vals return self - + class _DataAccess: def __init__(self, parent: "DataDictBase") -> None: self._parent = parent def __getattribute__(self, __name: str) -> Any: - parent = super(DataDictBase._DataAccess, self).__getattribute__('_parent') + parent = super(DataDictBase._DataAccess, self).__getattribute__("_parent") if __name in [k for k, _ in parent.data_items()]: return parent.data_vals(__name) @@ -754,10 +784,10 @@ def __getattribute__(self, __name: str) -> Any: def __setattr__(self, __name: str, __value: Any) -> None: # this check: make sure that we can set the parent correctly in the # constructor. - if hasattr(self, '_parent'): + if hasattr(self, "_parent"): if __name in [k for k, _ in self._parent.data_items()]: - self._parent[__name]['values'] = __value - + self._parent[__name]["values"] = __value + # still allow setting random things, essentially. else: super(DataDictBase._DataAccess, self).__setattr__(__name, __value) @@ -782,7 +812,7 @@ class DataDict(DataDictBase): instances. """ - def __add__(self, newdata: 'DataDict') -> 'DataDict': + def __add__(self, newdata: "DataDict") -> "DataDict": """ Adding two datadicts by appending each data array. @@ -798,16 +828,12 @@ def __add__(self, newdata: 'DataDict') -> 'DataDict': s = misc.unwrap_optional(self.structure(add_shape=False)) if DataDictBase.same_structure(self, newdata): for k, v in self.data_items(): - val0 = self[k]['values'] - val1 = newdata[k]['values'] - s[k]['values'] = np.append( - self[k]['values'], - newdata[k]['values'], - axis=0 + s[k]["values"] = np.append( + self[k]["values"], newdata[k]["values"], axis=0 ) return s else: - raise ValueError('Incompatible data structures.') + raise ValueError("Incompatible data structures.") def append(self, newdata: "DataDict") -> None: """ @@ -817,23 +843,18 @@ def append(self, newdata: "DataDict") -> None: :raises: ``ValueError``, if the structures are incompatible. """ if not DataDictBase.same_structure(self, newdata): - raise ValueError('Incompatible data structures.') + raise ValueError("Incompatible data structures.") newvals = {} for k, v in newdata.data_items(): - if isinstance(self[k]['values'], list) and isinstance( - v['values'], list): - newvals[k] = self[k]['values'] + v['values'] + if isinstance(self[k]["values"], list) and isinstance(v["values"], list): + newvals[k] = self[k]["values"] + v["values"] else: - newvals[k] = np.append( - self[k]['values'], - v['values'], - axis=0 - ) + newvals[k] = np.append(self[k]["values"], v["values"], axis=0) # only actually for k, v in newvals.items(): - self[k]['values'] = v + self[k]["values"] = v def add_data(self, **kw: Any) -> None: # TODO: fill non-given data with nan or none @@ -852,7 +873,7 @@ def add_data(self, **kw: Any) -> None: records = self.to_records(**kw) for name, datavals in records.items(): - dd[name]['values'] = datavals + dd[name]["values"] = datavals if dd.validate(): nrecords = self.nrecords() @@ -860,7 +881,7 @@ def add_data(self, **kw: Any) -> None: self.append(dd) else: for key, val in dd.data_items(): - self[key]['values'] = val['values'] + self[key]["values"] = val["values"] self.validate() # shape information and expansion @@ -873,7 +894,7 @@ def nrecords(self) -> Optional[int]: """ self.validate() for _, v in self.data_items(): - return len(v['values']) + return len(v["values"]) return None def _inner_shapes(self) -> Dict[str, Tuple[int, ...]]: @@ -912,7 +933,7 @@ def is_expandable(self) -> bool: else: return False - def expand(self) -> 'DataDict': + def expand(self) -> "DataDict": """ Expand nested values in the data fields. @@ -926,7 +947,7 @@ def expand(self) -> 'DataDict': """ self.validate() if not self.is_expandable(): - raise ValueError('Data cannot be expanded.') + raise ValueError("Data cannot be expanded.") struct = misc.unwrap_optional(self.structure(add_shape=False)) ret = DataDict(**struct) @@ -939,10 +960,9 @@ def expand(self) -> 'DataDict': for k, v in self.data_items(): reps = size // np.prod(ishp[k]) if reps > 1: - ret[k]['values'] = \ - self[k]['values'].repeat(reps, axis=0).reshape(-1) + ret[k]["values"] = self[k]["values"].repeat(reps, axis=0).reshape(-1) else: - ret[k]['values'] = self[k]['values'].reshape(-1) + ret[k]["values"] = self[k]["values"].reshape(-1) return ret @@ -961,23 +981,24 @@ def validate(self) -> bool: if super().validate(): nvals = None nvalsrc = None - msg = '\n' + msg = "\n" for n, v in self.data_items(): - if type(v['values']) not in [np.ndarray, - np.ma.core.MaskedArray]: - self[n]['values'] = np.array(v['values']) + if type(v["values"]) not in [np.ndarray, np.ma.core.MaskedArray]: + self[n]["values"] = np.array(v["values"]) if nvals is None: - nvals = len(v['values']) + nvals = len(v["values"]) nvalsrc = n else: - if len(v['values']) != nvals: - msg += " * '{}' has length {}, but have found {} in " \ - "'{}'\n".format( - n, len(v['values']), nvals, nvalsrc) - - if msg != '\n': + if len(v["values"]) != nvals: + msg += ( + " * '{}' has length {}, but have found {} in '{}'\n".format( + n, len(v["values"]), nvals, nvalsrc + ) + ) + + if msg != "\n": raise ValueError(msg) return True @@ -994,7 +1015,7 @@ def sanitize(self) -> "DataDict": ret = super().sanitize() return ret.remove_invalid_entries() - def remove_invalid_entries(self) -> 'DataDict': + def remove_invalid_entries(self) -> "DataDict": """ Remove all rows that are ``None`` or ``np.nan`` in *all* dependents. @@ -1005,7 +1026,6 @@ def remove_invalid_entries(self) -> 'DataDict': # collect rows that are completely invalid for d in self.dependents(): - # need to discriminate whether there are nested dims or not if len(ishp[d]) == 0: rows = self.data_vals(d) @@ -1019,7 +1039,9 @@ def remove_invalid_entries(self) -> 'DataDict': if len(ishp[d]) == 0: _newidxs = np.atleast_1d(np.asarray(rows is None)).nonzero()[0] else: - _newidxs = np.atleast_1d(np.asarray(np.all(rows is None, axis=-1))).nonzero()[0] + _newidxs = np.atleast_1d( + np.asarray(np.all(rows is None, axis=-1)) + ).nonzero()[0] _idxs = np.append(_idxs, _newidxs) # get indices for all rows that are fully NaN. works only @@ -1036,10 +1058,9 @@ def remove_invalid_entries(self) -> 'DataDict': idxs.append(_idxs) if len(idxs) > 0: - remove_idxs = reduce(np.intersect1d, - tuple(np.array(idxs).astype(int))) + remove_idxs = reduce(np.intersect1d, tuple(np.array(idxs).astype(int))) for k, v in self.data_items(): - v['values'] = np.delete(v['values'], remove_idxs, axis=0) + v["values"] = np.delete(v["values"], remove_idxs, axis=0) return self @@ -1076,44 +1097,44 @@ def validate(self) -> bool: if not super().validate(): return False - msg = '\n' + msg = "\n" axes = None - axessrc = '' + axessrc = "" for d in self.dependents(): if axes is None: axes = self.axes(d) else: if axes != self.axes(d): - msg += f" * All dependents must have the same axes, but " + msg += " * All dependents must have the same axes, but " msg += f"{d} has {self.axes(d)} and {axessrc} has {axes}\n" shp = None - shpsrc = '' + shpsrc = "" data_items = dict(self.data_items()) for n, v in data_items.items(): - if type(v['values']) not in [np.ndarray, np.ma.core.MaskedArray]: - self[n]['values'] = np.array(v['values']) + if type(v["values"]) not in [np.ndarray, np.ma.core.MaskedArray]: + self[n]["values"] = np.array(v["values"]) if shp is None: - shp = v['values'].shape + shp = v["values"].shape shpsrc = n else: - if v['values'].shape != shp: + if v["values"].shape != shp: msg += f" * shapes need to match, but '{n}' has" msg += f" {v['values'].shape}, " msg += f"and '{shpsrc}' has {shp}.\n" - if msg != '\n': + if msg != "\n": raise ValueError(msg) - if 'axes' in v: - for axis_num, na in enumerate(v['axes']): + if "axes" in v: + for axis_num, na in enumerate(v["axes"]): # check that the data of the axes matches its use # if data present - axis_data = data_items[na]['values'] + axis_data = data_items[na]["values"] # for the data to be a valid meshgrid, we need to have an increase/decrease along each # axis that contains data. @@ -1122,32 +1143,37 @@ def validate(self) -> bool: try: if axis_data.shape[axis_num] > 1: - steps = np.unique(np.sign(np.diff(axis_data, axis=axis_num))) - - # for incomplete data, there maybe nan steps -- we need to remove those, + steps = np.unique( + np.sign(np.diff(axis_data, axis=axis_num)) + ) + + # for incomplete data, there maybe nan steps -- we need to remove those, # doesn't mean anything is wrong. steps = steps[~np.isnan(steps)] - + if 0 in steps: - msg += (f"Malformed data: {na} is expected to be {axis_num}th " - "axis but has no variation along that axis.\n") + msg += ( + f"Malformed data: {na} is expected to be {axis_num}th " + "axis but has no variation along that axis.\n" + ) if steps.size > 1: - msg += (f"Malformed data: axis {na} is not monotonous.\n") - + msg += f"Malformed data: axis {na} is not monotonous.\n" + # can happen if we have bad shapes. but that should already have been caught. except IndexError: pass - if '__shape__' in v: - v['__shape__'] = shp + if "__shape__" in v: + v["__shape__"] = shp - if msg != '\n': + if msg != "\n": raise ValueError(msg) return True - def reorder_axes(self, data_names: Union[str, Sequence[str], None] = None, - **pos: int) -> 'MeshgridDataDict': + def reorder_axes( + self, data_names: Union[str, Sequence[str], None] = None, **pos: int + ) -> "MeshgridDataDict": """ Reorder the axes for all data. @@ -1174,34 +1200,34 @@ def reorder_axes(self, data_names: Union[str, Sequence[str], None] = None, for n in data_names: neworder, newaxes = orders[n] - self[n]['axes'] = newaxes - self[n]['values'] = self[n]['values'].transpose(neworder) + self[n]["axes"] = newaxes + self[n]["values"] = self[n]["values"].transpose(neworder) for ax in orig_axes[n]: if ax not in transposed: - self[ax]['values'] = self[ax]['values'].transpose(neworder) + self[ax]["values"] = self[ax]["values"].transpose(neworder) transposed.append(ax) self.validate() return self - - def mean(self, axis: str) -> 'MeshgridDataDict': + + def mean(self, axis: str) -> "MeshgridDataDict": """Take the mean over the given axis. - + :param axis: which axis to take the average over. :return: data, averaged over ``axis``. """ return _mesh_mean(self, axis) - - def slice(self, **kwargs: Dict[str, Union[slice, int]]) -> 'MeshgridDataDict': + + def slice(self, **kwargs: Dict[str, Union[slice, int]]) -> "MeshgridDataDict": """Return a N-d slice of the data. :param kwargs: slicing information in the format ``axis: spec``, where - ``spec`` can be a ``slice`` object, or an integer (usual slicing + ``spec`` can be a ``slice`` object, or an integer (usual slicing notation). :return: sliced data (as a copy) """ return _mesh_slice(self, **kwargs) - + def squeeze(self) -> None: """Remove size-1 dimensions.""" raise NotImplementedError @@ -1209,7 +1235,7 @@ def squeeze(self) -> None: def _mesh_mean(data: MeshgridDataDict, ax: str) -> MeshgridDataDict: """Average gridded data over one axis. - + :param data: input data :param ax: axis over which the average is performed; this dimension is removed from the result. @@ -1221,17 +1247,19 @@ def _mesh_mean(data: MeshgridDataDict, ax: str) -> MeshgridDataDict: for d, v in data.data_items(): if d in new_data: - new_data[d]['values'] = data.data_vals(d).mean(axis=iax) + new_data[d]["values"] = data.data_vals(d).mean(axis=iax) new_data.validate() return new_data -def _mesh_slice(data: MeshgridDataDict, **kwargs: Dict[str, Union[slice, int]]) -> MeshgridDataDict: +def _mesh_slice( + data: MeshgridDataDict, **kwargs: Dict[str, Union[slice, int]] +) -> MeshgridDataDict: """Return a N-d slice of the data. - + :param data: input data :param kwargs: slicing information in the format ``axis = spec``, where - ``spec`` can be a ``slice`` object, or an integer (usual slicing + ``spec`` can be a ``slice`` object, or an integer (usual slicing notation). :return: sliced data """ @@ -1243,15 +1271,17 @@ def _mesh_slice(data: MeshgridDataDict, **kwargs: Dict[str, Union[slice, int]]) assert isinstance(ret, MeshgridDataDict) for d, _ in data.data_items(): - ret[d]['values'] = data[d]['values'][tuple(slices)] + ret[d]["values"] = data[d]["values"][tuple(slices)] ret.validate() return ret # Tools for converting between different data types -def guess_shape_from_datadict(data: DataDict) -> \ - Dict[str, Union[None, Tuple[List[str], Tuple[int, ...]]]]: + +def guess_shape_from_datadict( + data: DataDict, +) -> Dict[str, Union[None, Tuple[List[str], Tuple[int, ...]]]]: """ Try to guess the shape of the datadict dependents from the axes values. @@ -1272,12 +1302,13 @@ def guess_shape_from_datadict(data: DataDict) -> \ return shapes -def datadict_to_meshgrid(data: DataDict, - target_shape: Union[Tuple[int, ...], None] = None, - inner_axis_order: Union[None, Sequence[str]] = None, - use_existing_shape: bool = False, - copy: bool = True) \ - -> MeshgridDataDict: +def datadict_to_meshgrid( + data: DataDict, + target_shape: Union[Tuple[int, ...], None] = None, + inner_axis_order: Union[None, Sequence[str]] = None, + use_existing_shape: bool = False, + copy: bool = True, +) -> MeshgridDataDict: """ Try to make a meshgrid from a dataset. @@ -1310,7 +1341,7 @@ def datadict_to_meshgrid(data: DataDict, return MeshgridDataDict() if not data.axes_are_compatible(): - raise GriddingError('Non-compatible axes, cannot grid that.') + raise GriddingError("Non-compatible axes, cannot grid that.") if not use_existing_shape and data.is_expandable(): data = data.expand() @@ -1320,13 +1351,15 @@ def datadict_to_meshgrid(data: DataDict, # guess what the shape likely is. if target_shape is None: shp_specs = guess_shape_from_datadict(data) - shps = set(order_shape[1] if order_shape is not None - else None for order_shape in shp_specs.values()) + shps = set( + order_shape[1] if order_shape is not None else None + for order_shape in shp_specs.values() + ) if len(shps) > 1: - raise GriddingError('Cannot determine unique shape for all data.') + raise GriddingError("Cannot determine unique shape for all data.") ret = list(shp_specs.values())[0] if ret is None: - raise GriddingError('Shape could not be inferred.') + raise GriddingError("Shape could not be inferred.") # the guess-function returns both axis order as well as shape. inner_axis_order, target_shape = ret @@ -1335,16 +1368,15 @@ def datadict_to_meshgrid(data: DataDict, axlist = data.axes(data.dependents()[0]) for k, v in data.data_items(): - vals = num.array1d_to_meshgrid(v['values'], target_shape, copy=copy) + vals = num.array1d_to_meshgrid(v["values"], target_shape, copy=copy) # if an inner axis order is given, we transpose to transform from that # to the specified order. if inner_axis_order is not None: - transpose_idxs = misc.reorder_indices( - inner_axis_order, axlist) + transpose_idxs = misc.reorder_indices(inner_axis_order, axlist) vals = vals.transpose(transpose_idxs) - newdata[k]['values'] = vals + newdata[k]["values"] = vals newdata = newdata.sanitize() newdata.validate() @@ -1360,8 +1392,8 @@ def meshgrid_to_datadict(data: MeshgridDataDict) -> DataDict: """ newdata = DataDict(**misc.unwrap_optional(data.structure(add_shape=False))) for k, v in data.data_items(): - val = v['values'].copy().reshape(-1) - newdata[k]['values'] = val + val = v["values"].copy().reshape(-1) + newdata[k]["values"] = val newdata = newdata.sanitize() newdata.validate() @@ -1370,6 +1402,7 @@ def meshgrid_to_datadict(data: MeshgridDataDict) -> DataDict: # Tools for manipulating and transforming data + def _find_replacement_name(ddict: DataDictBase, name: str) -> str: """ Find a replacement name for a data field that already exists in a @@ -1411,8 +1444,8 @@ def combine_datadicts(*dicts: DataDict) -> Union[DataDictBase, DataDict]: # axes in the return can be separated even if they match (caused # by earlier mismatches) - ret = None - rettype = None + ret: Optional[DataDictBase] = None + rettype: Optional[Type[DataDictBase]] = None for d in dicts: if ret is None: @@ -1420,14 +1453,14 @@ def combine_datadicts(*dicts: DataDict) -> Union[DataDictBase, DataDict]: rettype = type(d) else: - # if we don't have a well defined number of records anymore, # need to revert the type to DataDictBase - if hasattr(d, 'nrecords') and hasattr(ret, 'nrecords'): + if hasattr(d, "nrecords") and hasattr(ret, "nrecords"): if d.nrecords() != ret.nrecords(): rettype = DataDictBase else: rettype = DataDictBase + assert rettype is not None ret = rettype(**ret) # First, parse the axes in the to-be-added ddict. @@ -1457,9 +1490,9 @@ def combine_datadicts(*dicts: DataDict) -> Union[DataDictBase, DataDict]: else: newdep = d_dep - dep_axes = [ax_map[ax] for ax in d[d_dep]['axes']] + dep_axes = [ax_map[ax] for ax in d[d_dep]["axes"]] ret[newdep] = d[d_dep] - ret[newdep]['axes'] = dep_axes + ret[newdep]["axes"] = dep_axes if ret is None: ret = DataDict() @@ -1508,34 +1541,41 @@ def datastructure_from_string(description: str) -> DataDict: description = description.replace(" ", "") data_name_pattern = r"[a-zA-Z]+\w*(\[\w*\])?" - pattern = r"((?<=\A)|(?<=\;))" + data_name_pattern + r"(\((" + data_name_pattern + r"\,?)*\))?" + pattern = ( + r"((?<=\A)|(?<=\;))" + + data_name_pattern + + r"(\((" + + data_name_pattern + + r"\,?)*\))?" + ) r = re.compile(pattern) data_fields = [] - while (r.search(description)): + while r.search(description): match = r.search(description) - if match is None: break + if match is None: + break data_fields.append(description[slice(*match.span())]) - description = description[match.span()[1]:] + description = description[match.span()[1] :] dd: Dict[str, Any] = dict() def analyze_field(df: str) -> Tuple[str, Optional[str], Optional[List[str]]]: - has_unit = True if '[' in df and ']' in df else False - has_dependencies = True if '(' in df and ')' in df else False + has_unit = True if "[" in df and "]" in df else False + has_dependencies = True if "(" in df and ")" in df else False name: str = "" unit: Optional[str] = None axes: Optional[List[str]] = None if has_unit: - name = df.split('[')[0] - unit = df.split('[')[1].split(']')[0] + name = df.split("[")[0] + unit = df.split("[")[1].split("]")[0] if has_dependencies: - axes = df.split('(')[1].split(')')[0].split(',') + axes = df.split("(")[1].split(")")[0].split(",") elif has_dependencies: - name = df.split('(')[0] - axes = df.split('(')[1].split(')')[0].split(',') + name = df.split("(")[0] + axes = df.split("(")[1].split(")")[0].split(",") else: name = df @@ -1550,14 +1590,14 @@ def analyze_field(df: str) -> Tuple[str, Optional[str], Optional[List[str]]]: # if an independent is specified multiple times, units must not collide # (but units do not have to be specified more than once) if name in dd: - if 'axes' in dd[name] or axes is not None: - raise ValueError(f'{name} is specified more than once.') - if 'unit' in dd[name] and unit is not None and dd[name]['unit'] != unit: - raise ValueError(f'conflicting units for {name}') + if "axes" in dd[name] or axes is not None: + raise ValueError(f"{name} is specified more than once.") + if "unit" in dd[name] and unit is not None and dd[name]["unit"] != unit: + raise ValueError(f"conflicting units for {name}") dd[name] = dict() if unit is not None: - dd[name]['unit'] = unit + dd[name]["unit"] = unit if axes is not None: for ax in axes: @@ -1565,7 +1605,9 @@ def analyze_field(df: str) -> Tuple[str, Optional[str], Optional[List[str]]]: # we do not allow nested dependencies. if ax_axes is not None: - raise ValueError(f'{ax_name} is independent, may not have dependencies') + raise ValueError( + f"{ax_name} is independent, may not have dependencies" + ) # we can add fields implicitly from dependencies. # independents may be given both implicitly and explicitly, but only @@ -1573,23 +1615,29 @@ def analyze_field(df: str) -> Tuple[str, Optional[str], Optional[List[str]]]: if ax_name not in dd: dd[ax_name] = dict() if ax_unit is not None: - dd[ax_name]['unit'] = ax_unit + dd[ax_name]["unit"] = ax_unit else: - if 'unit' in dd[ax_name] and ax_unit is not None and dd[ax_name]['unit'] != ax_unit: - raise ValueError(f'conflicting units for {ax_name}') + if ( + "unit" in dd[ax_name] + and ax_unit is not None + and dd[ax_name]["unit"] != ax_unit + ): + raise ValueError(f"conflicting units for {ax_name}") - if 'axes' not in dd[name]: - dd[name]['axes'] = [] - dd[name]['axes'].append(ax_name) + if "axes" not in dd[name]: + dd[name]["axes"] = [] + dd[name]["axes"].append(ax_name) return DataDict(**dd) + #: shortcut to :func:`.datastructure_from_string`. str2dd = datastructure_from_string -def datasets_are_equal(a: DataDictBase, b: DataDictBase, - ignore_meta: bool = False) -> bool: +def datasets_are_equal( + a: DataDictBase, b: DataDictBase, ignore_meta: bool = False +) -> bool: """Check whether two datasets are equal. Compares type, structure, and content of all fields. @@ -1600,7 +1648,7 @@ def datasets_are_equal(a: DataDictBase, b: DataDictBase, :returns: ``True`` or ``False``. """ - if not type(a) == type(b): + if type(a) is not type(b): return False if not a.same_structure(a, b): @@ -1621,15 +1669,14 @@ def datasets_are_equal(a: DataDictBase, b: DataDictBase, # check all data fields in a for dn, dv in a.data_items(): - # are all fields also present in b? if dn not in [dnn for dnn, dvv in b.data_items()]: return False # check if data is equal if not num.arrays_equal( - np.array(a.data_vals(dn)), - np.array(b.data_vals(dn)), + np.array(a.data_vals(dn)), + np.array(b.data_vals(dn)), ): return False @@ -1654,7 +1701,7 @@ def datasets_are_equal(a: DataDictBase, b: DataDictBase, return True -def dd2df(dd: DataDict): +def dd2df(dd: DataDict) -> pd.DataFrame: """make a pandas Dataframe from a datadict. Uses MultiIndex, and assumes that all data fields are compatible. @@ -1670,10 +1717,10 @@ def dd2df(dd: DataDict): """ dd_flat = dd.expand() idx = pd.MultiIndex.from_arrays( - [dd_flat[a]['values'] for a in dd_flat.axes()], - names = dd_flat.axes(), + [dd_flat[a]["values"] for a in dd_flat.axes()], + names=dd_flat.axes(), ) - vals = {d: dd_flat[d]['values'] for d in dd_flat.dependents()} + vals = {d: dd_flat[d]["values"] for d in dd_flat.dependents()} return pd.DataFrame(data=vals, index=idx) @@ -1697,18 +1744,18 @@ def dd2xr(dd: MeshgridDataDict) -> xr.Dataset: axes = dd.axes() coords = {} for i, a in enumerate(axes): - slices = [0] * len(axes) + slices: list[Union[int, slice]] = [0] * len(axes) slices[i] = slice(None) - coords[a] = dd[a]['values'][tuple(slices)] - + coords[a] = dd[a]["values"][tuple(slices)] + xds = xr.Dataset( - {d: (axes, dd[d]['values']) for d in dd.dependents()}, + {d: (axes, dd[d]["values"]) for d in dd.dependents()}, coords=coords, ) - + for d in xds.data_vars: - xds[d].attrs['units'] = dd[d]['unit'] + xds[d].attrs["units"] = dd[d]["unit"] for d in xds.dims: - xds[d].attrs['units'] = dd[d]['unit'] + xds[d].attrs["units"] = dd[d]["unit"] return xds diff --git a/labcore/data/datadict_storage.py b/src/labcore/data/datadict_storage.py similarity index 88% rename from labcore/data/datadict_storage.py rename to src/labcore/data/datadict_storage.py index 5d0a7d9..f064bce 100644 --- a/labcore/data/datadict_storage.py +++ b/src/labcore/data/datadict_storage.py @@ -7,33 +7,34 @@ The lock file has the following format: ~.lock. The file lock will get deleted even if the program crashes. If the process is suddenly stopped however, we cannot guarantee that the file lock will be deleted. """ -import os -import logging -import time + import datetime -import uuid import json -import shutil +import logging +import os import re +import shutil +import time +import uuid from enum import Enum -from typing import Any, Union, Optional, Dict, Type, Collection, List -from types import TracebackType from pathlib import Path +from types import TracebackType +from typing import Any, Collection, Dict, List, Optional, Tuple, Type, Union -import numpy as np import h5py +import numpy as np +import pandas as pd import xarray as xr -from .tools import split_complex from .datadict import ( DataDict, - is_meta_key, - DataDictBase, - dd2xr, datadict_to_meshgrid, - dd2df, datasets_are_equal, + dd2df, + dd2xr, + is_meta_key, ) +from .tools import split_complex __author__ = "Wolfgang Pfaff" __license__ = "MIT" @@ -45,8 +46,9 @@ # FIXME: need correct handling of dtypes and list/array conversion + class NumpyEncoder(json.JSONEncoder): - def default(self, obj): + def default(self, obj: Any) -> Any: if isinstance(obj, np.ndarray): return obj.tolist() if isinstance(obj, np.integer): @@ -89,7 +91,7 @@ def h5ify(obj: Any) -> Any: if not all_string: obj = np.array(obj) - if type(obj) == np.ndarray and obj.dtype.kind == "U": + if type(obj) is np.ndarray and obj.dtype.kind == "U": return np.char.encode(obj, encoding="utf8") return obj @@ -101,16 +103,16 @@ def deh5ify(obj: Any) -> Any: :param obj: Input object. :return: Object """ - if type(obj) == bytes: + if type(obj) is bytes: return obj.decode() - if type(obj) == np.ndarray and obj.dtype.kind == "S": + if type(obj) is np.ndarray and obj.dtype.kind == "S": return np.char.decode(obj) return obj -def set_attr(h5obj: Any, name: str, val: Any) -> None: +def set_attr(h5obj: Union[h5py.File, h5py.Group], name: str, val: Any) -> None: """Set attribute `name` of object `h5obj` to `val` Use :func:`h5ify` to convert the object, then try to set the attribute @@ -125,7 +127,10 @@ def set_attr(h5obj: Any, name: str, val: Any) -> None: def add_cur_time_attr( - h5obj: Any, name: str = "creation", prefix: str = "__", suffix: str = "__" + h5obj: Union[h5py.File, h5py.Group], + name: str = "creation", + prefix: str = "__", + suffix: str = "__", ) -> None: """Add current time information to the given HDF5 object, following the format of: ``_time_sec``. @@ -340,7 +345,7 @@ def datadict_from_hdf5( def all_datadicts_from_hdf5( path: Union[str, Path], file_timeout: Optional[float] = None, **kwargs: Any -) -> Dict[str, Any]: +) -> Dict[str, DataDict]: """ Loads all the DataDicts contained on a single HDF5 file. Returns a dictionary with the group names as keys and the DataDicts as the values of that key. @@ -364,9 +369,11 @@ def all_datadicts_from_hdf5( return ret -def reconstruct_safe_write_data(path: Union[str, Path], - unification_from_scratch: bool = True, - file_timeout: Optional[float] = None) -> DataDictBase: +def reconstruct_safe_write_data( + path: Union[str, Path], + unification_from_scratch: bool = True, + file_timeout: Optional[float] = None, +) -> DataDict: """ Creates a new DataDict from the data saved in the .tmp folder. This is used when the data is saved in the safe writing mode. The data is saved in individual files in the .tmp folder. This function reconstructs the data from @@ -388,7 +395,9 @@ def reconstruct_safe_write_data(path: Union[str, Path], files = [] for dirpath, dirnames, filenames in os.walk(str(tmp_path)): - files.extend([(Path(dirpath)/file) for file in filenames if file.endswith(".ddh5")]) + files.extend( + [(Path(dirpath) / file) for file in filenames if file.endswith(".ddh5")] + ) files = sorted(files, key=lambda x: int(x.stem.split("#")[-1])) @@ -397,12 +406,16 @@ def reconstruct_safe_write_data(path: Union[str, Path], if path.exists() and not unification_from_scratch: dd = datadict_from_hdf5(path, file_timeout=file_timeout) if not dd.has_meta("last_reconstructed_file"): - raise ValueError("The file does not have the meta data 'last_reconstructed_file', " - "could not know where to reconstruct from.") + raise ValueError( + "The file does not have the meta data 'last_reconstructed_file', " + "could not know where to reconstruct from." + ) last_reconstructed_file = Path(dd.meta_val("last_reconstructed_file")) if not last_reconstructed_file.exists() or last_reconstructed_file not in files: - raise ValueError("When reconstructing the data, could find the last reconstructed file. " - "This indicates that something wrong happened in the tmp folder.") + raise ValueError( + "When reconstructing the data, could find the last reconstructed file. " + "This indicates that something wrong happened in the tmp folder." + ) starting_index = files.index(last_reconstructed_file) + 1 else: first = files.pop(0) @@ -416,7 +429,9 @@ def reconstruct_safe_write_data(path: Union[str, Path], # Add shape to axes for name, datavals in dd.data_items(): - datavals["__shape__"] = tuple(np.array(datavals["values"][:]).shape,) + datavals["__shape__"] = tuple( + np.array(datavals["values"][:]).shape, + ) # Catches the edge case where there is a single file in the .tmp folder. # This will not happen other than the first time, so it is ok to have that first variable there. @@ -427,6 +442,7 @@ def reconstruct_safe_write_data(path: Union[str, Path], return dd + # File access with locking @@ -616,16 +632,26 @@ def __exit__( if self.safe_write_mode: try: logger.debug("Starting reconstruction of data") - dd = reconstruct_safe_write_data(self.filepath, file_timeout=self.file_timeout) + dd = reconstruct_safe_write_data( + self.filepath, file_timeout=self.file_timeout + ) # Makes sure the reconstructed data matches the one in the .tmp folder assert datasets_are_equal(dd, self.datadict, ignore_meta=True) - datadict_to_hdf5(dd, self.filepath, groupname=self.groupname, file_timeout=self.file_timeout, append_mode=AppendMode.none) + datadict_to_hdf5( + dd, + self.filepath, + groupname=self.groupname, + file_timeout=self.file_timeout, + append_mode=AppendMode.none, + ) shutil.rmtree(self.filepath.parent / ".tmp") except Exception as e: - logger.error(f"Error while unifying data. Data should be located in the .tmp directory: {e}") + logger.error( + f"Error while unifying data. Data should be located in the .tmp directory: {e}" + ) self.add_tag("__not_reconstructed__") raise e @@ -670,7 +696,7 @@ def data_file_path(self) -> Path: return Path(data_folder_path, self.filename) - def _generate_next_safe_write_path(self): + def _generate_next_safe_write_path(self) -> Path: """ Generates the next path for the data to be saved in the safe writing mode. Should not be used for other things. """ @@ -678,6 +704,7 @@ def _generate_next_safe_write_path(self): now = datetime.datetime.now() # Creates tmp folder + assert self.filepath is not None tmp_folder = self.filepath.parent / ".tmp" tmp_folder.mkdir(exist_ok=True) @@ -701,7 +728,9 @@ def _generate_next_safe_write_path(self): keep_searching = True while keep_searching: n_secs += 1 - second_folder = minute_folder / (now.strftime("%S") + f"_#{str(n_secs)}") + second_folder = minute_folder / ( + now.strftime("%S") + f"_#{str(n_secs)}" + ) if not second_folder.exists(): keep_searching = False second_folder.mkdir() @@ -714,7 +743,7 @@ def _generate_next_safe_write_path(self): filename = now.strftime("%Y-%m-%d-%H_%M_%S") + f"_{n_secs}_#{self.n_files}.ddh5" self.n_files += 1 - return second_folder/filename + return second_folder / filename def add_data(self, **kwargs: Any) -> None: """Add data to the file (and the internal `DataDict`). @@ -731,6 +760,7 @@ def add_data(self, **kwargs: Any) -> None: if self.safe_write_mode: clean_dd_copy = self.datadict.structure() + assert clean_dd_copy is not None clean_dd_copy.add_data(**kwargs) filepath = self._generate_next_safe_write_path() @@ -745,18 +775,32 @@ def add_data(self, **kwargs: Any) -> None: delta_t = time.time() - self.last_reconstruction_time # Reconstructs the data every n_files_per_reconstruction files or every n_seconds_per_reconstruction seconds - if (self.n_files - self.last_update_n_files >= self.n_files_per_reconstruction or - delta_t > self.n_seconds_per_reconstruction): + if ( + self.n_files - self.last_update_n_files + >= self.n_files_per_reconstruction + or delta_t > self.n_seconds_per_reconstruction + ): try: - dd = reconstruct_safe_write_data(self.filepath, unification_from_scratch=False, - file_timeout=self.file_timeout) - datadict_to_hdf5(dd, self.filepath, groupname=self.groupname, file_timeout=self.file_timeout, append_mode=AppendMode.none) + assert self.filepath is not None + dd = reconstruct_safe_write_data( + self.filepath, + unification_from_scratch=False, + file_timeout=self.file_timeout, + ) + datadict_to_hdf5( + dd, + self.filepath, + groupname=self.groupname, + file_timeout=self.file_timeout, + append_mode=AppendMode.none, + ) + with FileOpener(self.filepath, "a", timeout=self.file_timeout) as f: + add_cur_time_attr(f, name="last_change") + add_cur_time_attr(f[self.groupname], name="last_change") except RuntimeError as e: - logger.warning(f"Error while unifying data: {e} \nData is still getting saved in .tmp directory.") - - with FileOpener(self.filepath, "a", timeout=self.file_timeout) as f: - add_cur_time_attr(f, name="last_change") - add_cur_time_attr(f[self.groupname], name="last_change") + logger.warning( + f"Error while unifying data: {e} \nData is still getting saved in .tmp directory." + ) # Even if I fail at reconstruction, I want to wait the same amount as if it was successful to try again. self.last_reconstruction_time = time.time() @@ -804,11 +848,14 @@ def save_dict(self, name: str, d: dict) -> None: json.dump(d, f, indent=4, ensure_ascii=False, cls=NumpyEncoder) -def data_info(folder: str, fn: str = "data.ddh5", do_print: bool = True): - fn = Path(folder, fn) - dataset = datadict_from_hdf5(fn) +def data_info( + folder: str, fn: str = "data.ddh5", do_print: bool = True +) -> Optional[str]: + fn_path = Path(folder, fn) + dataset = datadict_from_hdf5(fn_path) if do_print: print(dataset) + return None else: return str(dataset) @@ -823,11 +870,11 @@ def timestamp_from_path(p: Path) -> datetime.datetime: def find_data( - root, + root: Union[str, Path], newer_than: Optional[datetime.datetime] = None, older_than: Optional[datetime.datetime] = None, folder_filter: Optional[str] = None, -) -> List[Path]: +) -> Dict[Path, Tuple[List[str], List[str]]]: if not isinstance(root, Path): root = Path(root) @@ -850,7 +897,7 @@ def find_data( def most_recent_data_path( - root, + root: Union[str, Path], older_than: Optional[datetime.datetime] = None, folder_filter: Optional[str] = None, ) -> Path: @@ -859,7 +906,7 @@ def most_recent_data_path( def load_as_xr( - folder: Path, fn="data.ddh5", fields: Optional[List[str]] = None + folder: Path, fn: str = "data.ddh5", fields: Optional[List[str]] = None ) -> xr.Dataset: """Load ddh5 data as xarray (only for gridable data). @@ -875,17 +922,17 @@ def load_as_xr( _type_ _description_ """ - fn = folder / fn - dd = datadict_from_hdf5(fn) + fn_path = folder / fn + dd = datadict_from_hdf5(fn_path) if fields is not None: dd = dd.extract(fields) xrdata = split_complex(dd2xr(datadict_to_meshgrid(dd))) xrdata.attrs["raw_data_folder"] = str(folder.resolve()) - xrdata.attrs["raw_data_fn"] = str(fn) + xrdata.attrs["raw_data_fn"] = str(fn_path) return xrdata -def load_as_df(folder, fn="data.ddh5"): - fn = folder / fn - dfdata = split_complex(dd2df(datadict_from_hdf5(fn))) +def load_as_df(folder: Path, fn: str = "data.ddh5") -> pd.DataFrame: + fn_path = folder / fn + dfdata = split_complex(dd2df(datadict_from_hdf5(fn_path))) return dfdata diff --git a/labcore/data/datagen.py b/src/labcore/data/datagen.py similarity index 57% rename from labcore/data/datagen.py rename to src/labcore/data/datagen.py index 4e3dd79..d5e0573 100644 --- a/labcore/data/datagen.py +++ b/src/labcore/data/datagen.py @@ -1,8 +1,9 @@ +from abc import ABC, abstractmethod +from dataclasses import asdict, dataclass +from typing import Any import numpy as np - -from dataclasses import dataclass, asdict -from abc import ABC, abstractmethod +from numpy.typing import NDArray """ @@ -36,68 +37,73 @@ """ + @dataclass class DataGen(ABC): - noise_std : float = 1.0 - + noise_std: float = 1.0 + + @staticmethod @abstractmethod - def model(coordinates, *args, **kwargs): + def model(coordinates: NDArray[Any], *args: Any, **kwargs: Any) -> NDArray[Any]: pass - def generate(self, coordinates, **kwargs): - + def generate(self, coordinates: NDArray[Any], **kwargs: Any) -> NDArray[Any]: + # updates previously set dataclass fields - params = asdict(self) + params = asdict(self) params.update(kwargs) - noise_std = params.pop('noise_std') + noise_std = params.pop("noise_std") one_d = coordinates.ndim == 1 coordinates = np.atleast_2d(coordinates) - model_outputs = np.array([self.model(coords, **params) + - self.noise(coords, noise_std) - for coords in coordinates]) - if (one_d): + model_outputs = np.array( + [ + self.model(coords, **params) + self.noise(coords, noise_std) + for coords in coordinates + ] + ) + if one_d: return model_outputs.squeeze() return model_outputs - + @staticmethod - def noise(coordinates, std): - return np.random.normal(scale = std, size = len(coordinates)) - + def noise(coordinates: NDArray[Any], std: float) -> NDArray[Any]: + return np.random.normal(scale=std, size=len(coordinates)) + @dataclass class ExponentialDataGen(DataGen): - base: float = np.e @staticmethod - def model(coordinates, base): - return base ** coordinates - + def model(coordinates: NDArray[Any], base: float) -> NDArray[Any]: + return base**coordinates @dataclass class SineDataGen(DataGen): - - A : float = 1 - f : float = 1 - phi : float = 0 - of : float = 0 + A: float = 1 + f: float = 1 + phi: float = 0 + of: float = 0 @staticmethod - def model(coordinates, A, f, phi, of): + def model( + coordinates: NDArray[Any], A: float, f: float, phi: float, of: float + ) -> NDArray[Any]: return A * np.sin(2 * np.pi * coordinates * f + phi) + of @dataclass class GaussianDataGen(DataGen): - - x0 : float = 0 - sigma : float = 1 - A : float = 1 - of : float = 0 + x0: float = 0 + sigma: float = 1 + A: float = 1 + of: float = 0 @staticmethod - def model(coordinates, x0, sigma, A, of): - return A * np.exp(-((coordinates - x0) ** 2) / (2 * sigma ** 2)) + of \ No newline at end of file + def model( + coordinates: NDArray[Any], x0: float, sigma: float, A: float, of: float + ) -> NDArray[Any]: + return A * np.exp(-((coordinates - x0) ** 2) / (2 * sigma**2)) + of diff --git a/labcore/data/tools.py b/src/labcore/data/tools.py similarity index 92% rename from labcore/data/tools.py rename to src/labcore/data/tools.py index 8687178..9612384 100644 --- a/labcore/data/tools.py +++ b/src/labcore/data/tools.py @@ -1,10 +1,9 @@ -from typing import Union, Optional +from typing import Optional, Union import numpy as np import pandas as pd import xarray as xr - Data = Union[xr.Dataset, pd.DataFrame] """Type alias for valid data. Can be either a pandas DataFrame or an xarray Dataset.""" @@ -16,7 +15,7 @@ def data_dims(data: Optional[Data]) -> tuple[list[str], list[str]]: if isinstance(data, pd.DataFrame): return list(data.index.names), data.columns.to_list() elif isinstance(data, xr.Dataset): - return [str(c) for c in list(data.coords)], list(data.data_vars) + return [str(c) for c in list(data.coords)], [str(v) for v in data.data_vars] else: raise NotImplementedError diff --git a/src/labcore/measurement/__init__.py b/src/labcore/measurement/__init__.py new file mode 100644 index 0000000..68393cf --- /dev/null +++ b/src/labcore/measurement/__init__.py @@ -0,0 +1,31 @@ +from .record import ( + DataSpec as DataSpec, +) +from .record import ( + dep as dep, +) +from .record import ( + dependent as dependent, +) +from .record import ( + ds as ds, +) +from .record import ( + get_parameter as get_parameter, +) +from .record import ( + indep as indep, +) +from .record import ( + independent as independent, +) +from .record import ( + record_as as record_as, +) +from .record import ( + recording as recording, +) +from .sweep import Sweep as Sweep +from .sweep import once as once +from .sweep import pointer as pointer +from .sweep import sweep_parameter as sweep_parameter diff --git a/labcore/measurement/record.py b/src/labcore/measurement/record.py similarity index 77% rename from labcore/measurement/record.py rename to src/labcore/measurement/record.py index cdbc5ea..a1f8eae 100644 --- a/labcore/measurement/record.py +++ b/src/labcore/measurement/record.py @@ -1,47 +1,58 @@ -from dataclasses import dataclass, field -from typing import Optional, Iterable, List, Callable, Iterator, Tuple, Union, \ - Any, Dict -import inspect -from functools import update_wrapper -import copy import collections -from enum import Enum +import copy +import inspect import logging +from dataclasses import dataclass +from enum import Enum +from functools import update_wrapper +from typing import ( + Any, + Callable, + Dict, + Iterable, + Iterator, + List, + Tuple, + TypeGuard, + Union, +) try: from qcodes import Parameter as QCParameter + QCODES_PRESENT = True except ImportError: - QCParameter = None + QCParameter = None # type: ignore[assignment,misc] QCODES_PRESENT = False from ..utils.misc import map_input_to_signature - logger = logging.getLogger(__name__) class DataType(Enum): """Valid options for data types used in :class:`DataSpec`""" + #: scalar (single-valued) data. typically numeric, but also bool, etc. - scalar = 'scalar' + scalar = "scalar" #: multi-valued data. typically numpy-arrays. - array = 'array' + array = "array" @dataclass class DataSpec: """Specification for data parameters to be recorded.""" + #: name of the parameter name: str #: dependencies. if ``None``, it is independent. depends_on: Union[None, List[str], Tuple[str]] = None #: information about data format - type: Union[str, DataType] = 'scalar' + type: Union[str, DataType] = "scalar" #: physical unit of the data - unit: str = '' + unit: str = "" - def __post_init__(self): + def __post_init__(self) -> None: if isinstance(self.type, str): self.type = DataType(self.type) @@ -60,14 +71,12 @@ def __repr__(self) -> str: ds = DataSpec #: The type for creating a ds from a tuple (i.e., what can be passed to the #: constructor of :class:`.DataSpec`) -DataSpecFromTupleType = Tuple[str, Union[None, List[str], Tuple[str]], str, - str] +DataSpecFromTupleType = Tuple[str, Union[None, List[str], Tuple[str]], str, str] #: The type for creating a ds from a dict (i.e., what can be passed to the #: constructor of :class:`.DataSpec` as keywords) DataSpecFromDictType = Dict[str, Union[str, Union[None, List[str], Tuple[str]]]] #: The type from which we can create a DataSpec. -DataSpecCreationType = Union[str, DataSpecFromTupleType, - DataSpecFromDictType, DataSpec] +DataSpecCreationType = Union[str, DataSpecFromTupleType, DataSpecFromDictType, DataSpec] def data_specs_label(*dspecs: DataSpec) -> str: @@ -99,7 +108,7 @@ def make_data_spec(value: DataSpecCreationType) -> DataSpec: elif isinstance(value, (tuple, list)): return DataSpec(*value) elif isinstance(value, dict): - return DataSpec(**value) + return DataSpec(**value) # type: ignore[arg-type] elif isinstance(value, DataSpec): return value else: @@ -111,11 +120,10 @@ def make_data_specs(*specs: DataSpecCreationType) -> Tuple[DataSpec, ...]: :param specs: will be passed individually to :func:`.make_data_spec` """ - ret = [] + ret: List[DataSpec] = [] for spec in specs: ret.append(make_data_spec(spec)) - ret = tuple(ret) - return ret + return tuple(ret) def combine_data_specs(*specs: DataSpec) -> Tuple[DataSpec, ...]: @@ -130,7 +138,7 @@ def combine_data_specs(*specs: DataSpec) -> Tuple[DataSpec, ...]: return tuple(ret) -def independent(name: str, unit: str = '', type: str = 'scalar') -> DataSpec: +def independent(name: str, unit: str = "", type: str = "scalar") -> DataSpec: """Create a the spec for an independent parameter. All arguments are forwarded to the :class:`.DataSpec` constructor. ``depends_on`` is set to ``None``.""" @@ -140,8 +148,9 @@ def independent(name: str, unit: str = '', type: str = 'scalar') -> DataSpec: indep = independent -def dependent(name: str, depends_on: List[str] = [], unit: str = "", - type: str = 'scalar'): +def dependent( + name: str, depends_on: List[str] = [], unit: str = "", type: str = "scalar" +) -> DataSpec: """Create a the spec for a dependent parameter. All arguments are forwarded to the :class:`.DataSpec` constructor. ``depends_on`` may not be set to ``None``.""" @@ -157,38 +166,44 @@ def recording(*data_specs: DataSpecCreationType) -> Callable: """Returns a decorator that allows adding data parameter specs to a function. """ - def decorator(func): + + def decorator(func: Callable) -> FunctionToRecords: return FunctionToRecords(func, *make_data_specs(*data_specs)) + return decorator -def record_as(obj: Union[Callable, Iterable, Iterator], - *specs: DataSpecCreationType): +def record_as( + obj: Union[Callable, Iterable, Iterator], *specs: DataSpecCreationType +) -> Union["FunctionToRecords", "IteratorToRecords"]: """Annotate produced data as records. :param obj: a function that returns data or an iterable/iterator that produces data at each iteration step :param specs: specs for the data produced (see :func:`.make_data_specs`) """ - specs = make_data_specs(*specs) - if isinstance(obj, Callable): - return recording(*specs)(obj) + specs_ = make_data_specs(*specs) + if callable(obj): + return recording(*specs_)(obj) elif isinstance(obj, collections.abc.Iterable): - return IteratorToRecords(obj, *specs) + return IteratorToRecords(obj, *specs_) -def produces_record(obj: Any) -> bool: +def produces_record( + obj: Any, +) -> TypeGuard[Union["FunctionToRecords", "IteratorToRecords"]]: """Check if `obj` is annotated to generate records.""" - if hasattr(obj, 'get_data_specs'): + if hasattr(obj, "get_data_specs"): return True else: return False -def _to_record(value: Union[Dict, Iterable], - data_specs: Tuple[DataSpec]) -> Dict[str, Any]: +def _to_record( + value: Union[Dict, Iterable], data_specs: Tuple[DataSpec, ...] +) -> Union[Dict[str, Any], "IteratorToRecords"]: """Convert data to a record using the provided DataSpecs""" - ret = {} + ret: Any = {} if isinstance(value, dict): for s in data_specs: @@ -212,19 +227,18 @@ def _to_record(value: Union[Dict, Iterable], class IteratorToRecords: """A wrapper that converts the iteration values to records.""" - def __init__(self, iterable: Iterable, - *data_specs: DataSpecCreationType): + def __init__(self, iterable: Iterable, *data_specs: DataSpecCreationType): self.iterable = iterable self.data_specs = make_data_specs(*data_specs) - def get_data_specs(self): + def get_data_specs(self) -> Tuple[DataSpec, ...]: return self.data_specs - def __iter__(self): + def __iter__(self) -> Iterator[Any]: for val in self.iterable: yield _to_record(val, self.data_specs) - def __repr__(self): + def __repr__(self) -> str: from .sweep import CombineSweeps ret = self.iterable.__repr__() @@ -238,33 +252,35 @@ def __repr__(self): class FunctionToRecords: """A wrapper that converts a function return to a record.""" - def __init__(self, func, *data_specs): + def __init__(self, func: Callable, *data_specs: DataSpecCreationType) -> None: self.func = func self.func_sig = inspect.signature(self.func) self.data_specs = make_data_specs(*data_specs) + self.__name__: str = func.__name__ update_wrapper(self, func) self._args: List[Any] = [] self._kwargs: Dict[str, Any] = {} - def get_data_specs(self): + def get_data_specs(self) -> Tuple[DataSpec, ...]: return self.data_specs - def __call__(self, *args, **kwargs): - args = tuple(self._args + list(args)) + def __call__(self, *args: Any, **kwargs: Any) -> Any: + all_args = tuple(self._args + list(args)) kwargs.update(self._kwargs) - func_args, func_kwargs = map_input_to_signature(self.func_sig, - *args, **kwargs) + func_args, func_kwargs = map_input_to_signature( + self.func_sig, *all_args, **kwargs + ) ret = self.func(*func_args, **func_kwargs) return _to_record(ret, self.get_data_specs()) - def __repr__(self): + def __repr__(self) -> str: dnames = data_specs_label(*self.data_specs) ret = self.func.__name__ + str(self.func_sig) ret += f" as {dnames}" return ret - def using(self, *args, **kwargs) -> "FunctionToRecords": + def using(self, *args: Any, **kwargs: Any) -> "FunctionToRecords": """Set the default positional and keyword arguments that will be used when the function is called. @@ -281,7 +297,9 @@ def using(self, *args, **kwargs) -> "FunctionToRecords": # inherit shapes, dependencies (for ParameterWithSetPoints, for example) # needs a function to make data specs from parameters (incl some user # customization, like setting to array for regular parameters) -def get_parameter(param: QCParameter): +def get_parameter( + param: QCParameter, +) -> Union["FunctionToRecords", "IteratorToRecords"]: if not QCODES_PRESENT: raise RuntimeError("qcodes not found.") diff --git a/labcore/measurement/storage.py b/src/labcore/measurement/storage.py similarity index 74% rename from labcore/measurement/storage.py rename to src/labcore/measurement/storage.py index 91de5d7..b04ecac 100644 --- a/labcore/measurement/storage.py +++ b/src/labcore/measurement/storage.py @@ -14,28 +14,25 @@ data keys are given exactly like in the DataDict, i.e., incl the double underscore pre- and suffix. """ -import os -import time -from enum import Enum -from typing import Any, Union, Optional, Dict, Type, Collection, List, Tuple -from types import TracebackType -from pathlib import Path + +import glob import json +import logging +import os import pickle import shutil -import glob -import logging +import time +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple, Union import numpy as np -import h5py -from ..data.datadict import DataDict, is_meta_key +from ..data.datadict import DataDict from ..data.datadict_storage import DDH5Writer - from .sweep import Sweep -__author__ = 'Wolfgang Pfaff' -__license__ = 'MIT' +__author__ = "Wolfgang Pfaff" +__license__ = "MIT" TIMESTRFORMAT = "%Y-%m-%dT%H%M%S" @@ -52,7 +49,6 @@ def _create_datadict_structure(sweep: Sweep) -> DataDict: data_specs = sweep.get_data_specs() data_dict = DataDict() for spec in data_specs: - depends_on = spec.depends_on unit = spec.unit name = spec.name @@ -93,35 +89,37 @@ def _check_none(line: Dict, all: bool = True) -> bool: def _save_dictionary(dict: Dict, filepath: str) -> None: - with open(filepath, 'w') as f: + with open(filepath, "w") as f: json.dump(dict, f, indent=2, sort_keys=True, cls=NumpyEncoder) -def _pickle_and_save(obj, filepath: str) -> None: +def _pickle_and_save(obj: Any, filepath: str) -> None: try: - with open(filepath, 'wb') as f: + with open(filepath, "wb") as f: pickle.dump(obj, f) except TypeError as pickle_error: - print(f'Object could not be pickled: {pickle_error.args}') + print(f"Object could not be pickled: {pickle_error.args}") class NumpyEncoder(json.JSONEncoder): - def default(self, obj): + def default(self, obj: Any) -> Any: if isinstance(obj, np.ndarray): return obj.tolist() return json.JSONEncoder.default(self, obj) -def run_and_save_sweep(sweep: Sweep, - data_dir: str, - name: str, - ignore_all_None_results: bool = True, - save_action_kwargs: bool = False, - add_timestamps = False, - archive_files: Optional[List[str]] = None, - return_data: bool = False, - safe_write_mode: bool = False, - **extra_saving_items) -> Tuple[Union[str, Path], Optional[DataDict]]: +def run_and_save_sweep( + sweep: Sweep, + data_dir: str, + name: str, + ignore_all_None_results: bool = True, + save_action_kwargs: bool = False, + add_timestamps: bool = False, + archive_files: Optional[List[str]] = None, + return_data: bool = False, + safe_write_mode: bool = False, + **extra_saving_items: Any, +) -> Tuple[Union[str, Path], Optional[DataDict]]: """ Iterates through a sweep, saving the data coming through it into a file called at directory. @@ -153,13 +151,15 @@ def run_and_save_sweep(sweep: Sweep, data_dict = _create_datadict_structure(sweep) # Creates a file even when it fails. - with DDH5Writer(data_dict, data_dir, name=name, safe_write_mode=safe_write_mode) as writer: - + with DDH5Writer( + data_dict, data_dir, name=name, safe_write_mode=safe_write_mode + ) as writer: # Saving meta-data + assert writer.filepath is not None dir: Path = writer.filepath.parent if add_timestamps: t = time.localtime() - time_stamp = time.strftime(TIMESTRFORMAT, t) + '_' + time_stamp = time.strftime(TIMESTRFORMAT, t) + "_" for key, val in extra_saving_items.items(): if callable(val): @@ -168,11 +168,11 @@ def run_and_save_sweep(sweep: Sweep, value = val if add_timestamps: - pickle_path_file = os.path.join(dir, time_stamp + key + '.pickle') - json_path_file = os.path.join(dir, time_stamp + key + '.json') + pickle_path_file = os.path.join(dir, time_stamp + key + ".pickle") + json_path_file = os.path.join(dir, time_stamp + key + ".json") else: - pickle_path_file = os.path.join(dir, key + '.pickle') - json_path_file = os.path.join(dir, key + '.json') + pickle_path_file = os.path.join(dir, key + ".pickle") + json_path_file = os.path.join(dir, key + ".json") if isinstance(value, dict): try: @@ -182,8 +182,10 @@ def run_and_save_sweep(sweep: Sweep, if os.path.isfile(json_path_file): os.remove(json_path_file) - logging.info(f'{key} has not been able to save to json: {error.args}.' - f' The item will be pickled instead.') + logging.info( + f"{key} has not been able to save to json: {error.args}." + f" The item will be pickled instead." + ) _pickle_and_save(value, pickle_path_file) else: _pickle_and_save(value, pickle_path_file) @@ -191,33 +193,43 @@ def run_and_save_sweep(sweep: Sweep, # Save the kwargs if save_action_kwargs: if add_timestamps: - json_path_file = os.path.join(dir, time_stamp + 'sweep_action_kwargs.json') + json_path_file = os.path.join( + dir, time_stamp + "sweep_action_kwargs.json" + ) else: - json_path_file = os.path.join(dir, 'sweep_action_kwargs.json') + json_path_file = os.path.join(dir, "sweep_action_kwargs.json") _save_dictionary(sweep.action_kwargs, json_path_file) # Save archive_files if archive_files is not None: - archive_files_dir = os.path.join(dir, 'archive_files') + archive_files_dir = os.path.join(dir, "archive_files") os.mkdir(archive_files_dir) - if not isinstance(archive_files, list) and not isinstance(archive_files, tuple): + if not isinstance(archive_files, list) and not isinstance( + archive_files, tuple + ): if isinstance(archive_files, str): archive_files = [archive_files] else: - raise TypeError(f'{type(archive_files)} is not a list.') + raise TypeError(f"{type(archive_files)} is not a list.") for path in archive_files: if os.path.isdir(path): folder_name = os.path.basename(path) - if folder_name == '': + if folder_name == "": folder_name = os.path.basename(os.path.dirname(path)) - shutil.copytree(path, os.path.join(archive_files_dir, folder_name), dirs_exist_ok=True) + shutil.copytree( + path, + os.path.join(archive_files_dir, folder_name), + dirs_exist_ok=True, + ) elif os.path.isfile(path): shutil.copy(path, archive_files_dir) else: matches = glob.glob(path, recursive=True) if len(matches) == 0: - logging.info(f'{path} could not be found. Measurement will continue without archiving {path}') + logging.info( + f"{path} could not be found. Measurement will continue without archiving {path}" + ) for file in matches: shutil.copy(file, archive_files_dir) @@ -227,11 +239,14 @@ def run_and_save_sweep(sweep: Sweep, if not _check_none(line, all=ignore_all_None_results): writer.add_data(**line) except KeyboardInterrupt: - logger.warning('Sweep stopped by Keyboard interrupt. Data completed before interrupt should be saved.') + logger.warning( + "Sweep stopped by Keyboard interrupt. Data completed before interrupt should be saved." + ) ret = (dir, data_dict) if return_data else (dir, None) return ret - logger.info('The measurement has finished successfully and all of the data has been saved.') + logger.info( + "The measurement has finished successfully and all of the data has been saved." + ) ret = (dir, data_dict) if return_data else (dir, None) return ret - diff --git a/labcore/measurement/sweep.py b/src/labcore/measurement/sweep.py similarity index 68% rename from labcore/measurement/sweep.py rename to src/labcore/measurement/sweep.py index 1624536..7cb61ba 100644 --- a/labcore/measurement/sweep.py +++ b/src/labcore/measurement/sweep.py @@ -1,56 +1,72 @@ -import itertools -import inspect -from typing import Iterable, Callable, Union, Tuple, Any, Optional, Dict, List, Generator import collections -import logging -from functools import wraps, update_wrapper, partial import copy +import itertools +import logging +from functools import partial, update_wrapper +from typing import ( + Any, + Callable, + Dict, + Generator, + Iterable, + Iterator, + Optional, + Tuple, + Union, + cast, +) try: from qcodes import Parameter as QCParameter + QCODES_PRESENT = True except ImportError: - QCParameter = None + QCParameter = None # type: ignore[assignment,misc] QCODES_PRESENT = False -from .record import produces_record, DataSpec, IteratorToRecords, \ - DataSpecFromTupleType, record_as, combine_data_specs, independent, \ - make_data_spec, data_specs_label, DataSpecCreationType, make_data_specs, \ - FunctionToRecords, map_input_to_signature from ..utils.misc import indent_text - +from .record import ( + DataSpec, + DataSpecCreationType, + DataSpecFromTupleType, + FunctionToRecords, + IteratorToRecords, + combine_data_specs, + data_specs_label, + independent, + make_data_spec, + map_input_to_signature, + produces_record, + record_as, +) logger = logging.getLogger(__name__) -if QCODES_PRESENT: - ParamSpecType = Union[str, QCParameter, DataSpecFromTupleType, DataSpec] -else: - ParamSpecType = Union[str, DataSpecFromTupleType, DataSpec] +ParamSpecType = Union[str, Any, DataSpecFromTupleType, DataSpec] # Pointer tools class PointerFunction(FunctionToRecords): """A class that allows using a generator function as a pointer.""" - def _iterator2records(self, *args, **kwargs): - func_args, func_kwargs = map_input_to_signature(self.func_sig, - *args, **kwargs) + def _iterator2records(self, *args: Any, **kwargs: Any) -> Any: + func_args, func_kwargs = map_input_to_signature(self.func_sig, *args, **kwargs) ret = record_as(self.func(*func_args, **func_kwargs), *self.data_specs) return ret - def __call__(self, *args, **kwargs): - args = tuple(self._args + list(args)) + def __call__(self, *args: Any, **kwargs: Any) -> Any: + all_args = tuple(self._args + list(args)) kwargs.update(self._kwargs) - return self._iterator2records(*args, **kwargs) + return self._iterator2records(*all_args, **kwargs) - def __iter__(self): + def __iter__(self) -> Iterator: return iter(self._iterator2records(*self._args, **self._kwargs)) - def get_data_specs(self): + def get_data_specs(self) -> Tuple[DataSpec, ...]: return self.data_specs - def using(self, *args, **kwargs) -> "PointerFunction": + def using(self, *args: Any, **kwargs: Any) -> "PointerFunction": """Set the default positional and keyword arguments that will be used when the function is called. @@ -65,8 +81,10 @@ def using(self, *args, **kwargs) -> "PointerFunction": def pointer(*data_specs: DataSpecCreationType) -> Callable: """Create a decorator for functions that return pointer generators.""" + def decorator(func: Callable) -> PointerFunction: return PointerFunction(func, *data_specs) + return decorator @@ -84,8 +102,9 @@ def once(action: Callable) -> "Sweep": return Sweep(null_pointer, action) -def sweep_parameter(param: ParamSpecType, sweep_iterable: Iterable, - *actions: Callable) -> "Sweep": +def sweep_parameter( + param: ParamSpecType, sweep_iterable: Iterable, *actions: Callable +) -> "Sweep": """Create a sweep over a parameter. :param param: One of: @@ -103,17 +122,19 @@ def sweep_parameter(param: ParamSpecType, sweep_iterable: Iterable, if isinstance(param, str): param_ds = independent(param) elif isinstance(param, (tuple, list)): - param_ds = make_data_spec(*param) + param_ds = make_data_spec(param) # type: ignore[arg-type] elif isinstance(param, DataSpec): param_ds = param elif QCODES_PRESENT and isinstance(param, QCParameter): param_ds = independent(param.name, unit=param.unit) - def setfunc(*args, **kwargs): + def setfunc(*args: Any, **kwargs: Any) -> None: param.set(kwargs.get(param.name)) - actions = list(actions) - actions.insert(0, setfunc) + actions_list: list[Callable] = list(actions) + actions_list.insert(0, setfunc) + record_iterator = IteratorToRecords(sweep_iterable, param_ds) + return Sweep(record_iterator, *actions_list) else: raise TypeError(f"Cannot make parameter from type {type(param)}") @@ -121,7 +142,7 @@ def setfunc(*args, **kwargs): return Sweep(record_iterator, *actions) -def null_action(): +def null_action() -> None: return None @@ -153,37 +174,38 @@ class Sweep: # TODO: Add the rules. @staticmethod - def update_option_dict(src: Dict[str, Any], target: Dict[str, Any], level: int) -> None: - """Rules: work in progress :). - """ + def update_option_dict( + src: Dict[str, Any], target: Dict[str, Any], level: int + ) -> None: + """Rules: work in progress :).""" if not isinstance(src, dict) or not isinstance(target, dict): - raise ValueError('inputs need to be dictionaries.') + raise ValueError("inputs need to be dictionaries.") for k, v in src.items(): if k in target: if isinstance(v, dict) and level > 0: - Sweep.update_option_dict(src[k], target[k], level=level-1) + Sweep.update_option_dict(src[k], target[k], level=level - 1) else: target[k] = v @staticmethod - def propagate_sweep_options(sweep: "Sweep"): + def propagate_sweep_options(sweep: "Sweep") -> None: try: - first = sweep.pointer.iterable.first + first = sweep.pointer.iterable.first # type: ignore[attr-defined] Sweep.copy_sweep_options(sweep, first) except AttributeError: pass try: - second = sweep.pointer.iterable.second + second = sweep.pointer.iterable.second # type: ignore[attr-defined] Sweep.copy_sweep_options(sweep, second) except AttributeError: pass @staticmethod - def copy_sweep_options(src: "Sweep", target: Optional["Sweep"]): - if src is target: + def copy_sweep_options(src: "Sweep", target: Optional["Sweep"]) -> None: + if src is target or target is None: return Sweep.update_option_dict(src._action_kwargs, target._action_kwargs, level=2) @@ -192,64 +214,64 @@ def copy_sweep_options(src: "Sweep", target: Optional["Sweep"]): @staticmethod def link_sweep_properties(src: "Sweep", target: "Sweep") -> None: """Share state properties between sweeps.""" - for p in ['_state', '_pass_kwargs']: + for p in ["_state", "_pass_kwargs"]: if hasattr(src, p): setattr(target, p, getattr(src, p)) - iterable = getattr(target.pointer, 'iterable', None) - if iterable is not None and hasattr(iterable, 'first'): - first = getattr(iterable, 'first') + iterable = getattr(target.pointer, "iterable", None) + if iterable is not None and hasattr(iterable, "first"): + first = getattr(iterable, "first") setattr(first, p, getattr(src, p)) - if iterable is not None and hasattr(iterable, 'second'): - second = getattr(iterable, 'second') + if iterable is not None and hasattr(iterable, "second"): + second = getattr(iterable, "second") setattr(second, p, getattr(src, p)) Sweep.copy_sweep_options(src, target) - def __init__(self, pointer: Optional[Iterable], *actions: Callable): + def __init__(self, pointer: Optional[Iterable], *actions: Callable) -> None: """Constructor of :class:`.Sweep`.""" - self._state = {} - self._pass_kwargs = {} - self._action_kwargs = {} + self._state: Dict[str, Any] = {} + self._pass_kwargs: Dict[str, Any] = {} + self._action_kwargs: Dict[str, Any] = {} if pointer is None: - self.pointer = null_pointer + self.pointer: Iterable = null_pointer elif isinstance(pointer, (collections.abc.Iterable, Sweep)): self.pointer = pointer else: - raise TypeError('pointer needs to be iterable.') + raise TypeError("pointer needs to be iterable.") - self.actions = [] + self.actions: list[FunctionToRecords] = [] for a in actions: self.append_action(a) @property - def state(self): + def state(self) -> Dict[str, Any]: return self._state @state.setter - def state(self, value: Dict[str, Any]): + def state(self, value: Dict[str, Any]) -> None: for k, v in value.items(): self._state[k] = v @property - def pass_kwargs(self): + def pass_kwargs(self) -> Dict[str, Any]: return self._pass_kwargs @pass_kwargs.setter - def pass_kwargs(self, value: Dict[str, Any]): + def pass_kwargs(self, value: Dict[str, Any]) -> None: for k, v in value.items(): self._pass_kwargs[k] = v @property - def action_kwargs(self): + def action_kwargs(self) -> Dict[str, Any]: return self._action_kwargs @action_kwargs.setter - def action_kwargs(self, value: Dict[str, Any]): + def action_kwargs(self, value: Dict[str, Any]) -> None: for k, v in value.items(): self._action_kwargs[k] = v - def __iter__(self): + def __iter__(self) -> Iterator[Any]: return self.run() def __add__(self, other: Union[Callable, "Sweep"]) -> "Sweep": @@ -258,8 +280,9 @@ def __add__(self, other: Union[Callable, "Sweep"]) -> "Sweep": elif callable(other): sweep2 = Sweep(None, other) else: - raise TypeError(f'can only combine with Sweep or callable, ' - f'not {type(other)}') + raise TypeError( + f"can only combine with Sweep or callable, not {type(other)}" + ) Sweep.link_sweep_properties(self, sweep2) return append_sweeps(self, sweep2) @@ -270,8 +293,9 @@ def __mul__(self, other: Union[Callable, "Sweep"]) -> "Sweep": elif callable(other): sweep2 = Sweep(self.pointer, other) else: - raise TypeError(f'can only combine with Sweep or callable, ' - f'not {type(other)}') + raise TypeError( + f"can only combine with Sweep or callable, not {type(other)}" + ) Sweep.link_sweep_properties(self, sweep2) return zip_sweeps(self, sweep2) @@ -282,21 +306,22 @@ def __matmul__(self, other: Union[Callable, "Sweep"]) -> "Sweep": elif callable(other): sweep2 = Sweep(None, other) else: - raise TypeError(f'can only combine with Sweep or callable, ' - f'not {type(other)}') + raise TypeError( + f"can only combine with Sweep or callable, not {type(other)}" + ) Sweep.link_sweep_properties(self, sweep2) return nest_sweeps(self, sweep2) - def append_action(self, action: Callable): + def append_action(self, action: Callable) -> None: """Add an action to the sweep.""" if callable(action): if produces_record(action): - self.actions.append(action) + self.actions.append(cast(FunctionToRecords, action)) else: - self.actions.append(record_as(action)) + self.actions.append(cast(FunctionToRecords, record_as(action))) else: - raise TypeError('action must be a callable.') + raise TypeError("action must be a callable.") def run(self) -> "SweepIterator": """Create the iterator for the sweep.""" @@ -304,11 +329,12 @@ def run(self) -> "SweepIterator": self, state=self.state, pass_kwargs=self.pass_kwargs, - action_kwargs=self.action_kwargs) + action_kwargs=self.action_kwargs, + ) # FIXME: currently this only works for actions -- should be used also # for pointer funcs? - def set_options(self, **action_kwargs: Dict[str, Any]): + def set_options(self, **action_kwargs: Any) -> None: """Configure the sweep actions. :param action_kwargs: Keyword arguments to pass to action functions @@ -321,27 +347,30 @@ def set_options(self, **action_kwargs: Dict[str, Any]): def get_data_specs(self) -> Tuple[DataSpec, ...]: """Return the data specs of the sweep.""" - specs = [] - pointer_specs = [] + specs: list[DataSpec] = [] + pointer_specs: Tuple[DataSpec, ...] = () if produces_record(self.pointer): pointer_specs = self.pointer.get_data_specs() - specs = combine_data_specs(*(list(specs) + list(pointer_specs))) + specs = list(combine_data_specs(*(list(specs) + list(pointer_specs)))) for a in self.actions: if produces_record(a): action_specs = a.get_data_specs() - pointer_independents = [ds.name for ds in pointer_specs - if ds.depends_on is None] + pointer_independents = [ + ds.name for ds in pointer_specs if ds.depends_on is None + ] for aspec in action_specs: aspec_ = aspec.copy() if aspec_.depends_on is not None: - aspec_.depends_on = pointer_independents + aspec_.depends_on + aspec_.depends_on = pointer_independents + list( + aspec_.depends_on + ) - specs = combine_data_specs(*(list(specs) + [aspec_])) + specs = list(combine_data_specs(*(list(specs) + [aspec_]))) return tuple(specs) - def __repr__(self): + def __repr__(self) -> str: ret = self.pointer.__repr__() for a in self.actions: ret += f" >> {a.__repr__()}" @@ -356,16 +385,20 @@ class SweepIterator: functions. Manages and updates the state of the sweep. """ - def __init__(self, sweep: Sweep, - state: Dict[str, Any], - pass_kwargs=Dict[str, Any], - action_kwargs=Dict[str, Dict[str, Any]]): + def __init__( + self, + sweep: Sweep, + state: Dict[str, Any], + pass_kwargs: Dict[str, Any] = {}, + action_kwargs: Dict[str, Any] = {}, + ) -> None: self.sweep = sweep self.state = state self.pass_kwargs = pass_kwargs self.action_kwargs = action_kwargs + self.pointer: Iterator[Any] if isinstance(self.sweep.pointer, Sweep): self.pointer = iter(self.sweep.pointer) elif isinstance(self.sweep.pointer, collections.abc.Iterator): @@ -373,15 +406,15 @@ def __init__(self, sweep: Sweep, elif isinstance(self.sweep.pointer, collections.abc.Iterable): self.pointer = iter(self.sweep.pointer) else: - raise TypeError('pointer needs to be iterable.') + raise TypeError("pointer needs to be iterable.") - def __next__(self): - ret = {} + def __next__(self) -> Dict[str, Any]: + ret: Dict[str, Any] = {} next_point = next(self.pointer) if produces_record(self.sweep.pointer): ret.update(next_point) - pass_args = [] + pass_args: list[Any] = [] if self.sweep.pass_on_returns: if isinstance(next_point, (tuple, list)): if not self.sweep.pass_on_none: @@ -390,8 +423,9 @@ def __next__(self): pass_args = list(next_point) elif isinstance(next_point, dict): if not self.sweep.pass_on_none: - self.pass_kwargs.update({k: v for k, v in next_point.items() - if v is not None}) + self.pass_kwargs.update( + {k: v for k, v in next_point.items() if v is not None} + ) else: self.pass_kwargs.update(next_point) else: @@ -402,8 +436,7 @@ def __next__(self): this_action_kwargs = {} if self.sweep.pass_on_returns: this_action_kwargs.update(self.pass_kwargs) - this_action_kwargs.update( - self.action_kwargs.get(a.__name__, {})) + this_action_kwargs.update(self.action_kwargs.get(a.__name__, {})) action_return = a(*pass_args, **this_action_kwargs) if produces_record(a): @@ -411,8 +444,9 @@ def __next__(self): # actions always return records, so no need to worry about args if not self.sweep.pass_on_none: - self.pass_kwargs.update({k: v for k, v in action_return.items() - if v is not None}) + self.pass_kwargs.update( + {k: v for k, v in action_return.items() if v is not None} + ) else: self.pass_kwargs.update(action_return) @@ -423,6 +457,9 @@ def __next__(self): return ret + def __iter__(self) -> "SweepIterator": + return self + def append_sweeps(first: Sweep, second: Sweep) -> Sweep: """Append two sweeps. @@ -432,8 +469,9 @@ def append_sweeps(first: Sweep, second: Sweep) -> Sweep: """ both = IteratorToRecords( AppendSweeps(first, second), - *combine_data_specs(*(list(first.get_data_specs()) - + list(second.get_data_specs()))) + *combine_data_specs( + *(list(first.get_data_specs()) + list(second.get_data_specs())) + ), ) sweep = Sweep(both) Sweep.link_sweep_properties(first, sweep) @@ -448,8 +486,9 @@ def zip_sweeps(first: Sweep, second: Sweep) -> Sweep: """ both = IteratorToRecords( ZipSweeps(first, second), - *combine_data_specs(*(list(first.get_data_specs()) - + list(second.get_data_specs()))) + *combine_data_specs( + *(list(first.get_data_specs()) + list(second.get_data_specs())) + ), ) sweep = Sweep(both) Sweep.link_sweep_properties(first, sweep) @@ -468,11 +507,11 @@ def nest_sweeps(outer: Sweep, inner: Sweep) -> Sweep: inner_specs = [s.copy() for s in inner.get_data_specs()] for s in inner_specs: if s.depends_on is not None: - s.depends_on = outer_indeps + s.depends_on + s.depends_on = outer_indeps + list(s.depends_on) nested = IteratorToRecords( NestSweeps(outer, inner), - *combine_data_specs(*(list(outer_specs) + inner_specs)) + *combine_data_specs(*(list(outer_specs) + inner_specs)), ) sweep = Sweep(nested) Sweep.link_sweep_properties(outer, sweep) @@ -480,63 +519,59 @@ def nest_sweeps(outer: Sweep, inner: Sweep) -> Sweep: class CombineSweeps: + _operator_symbol: Optional[str] = None - _operator_symbol = None - - def __init__(self, first: Sweep, second: Sweep): + def __init__(self, first: Sweep, second: Sweep) -> None: self.first = first self.second = second - def __iter__(self): + def __iter__(self) -> Iterator[Dict[str, Any]]: raise NotImplementedError - def __repr__(self): + def __repr__(self) -> str: ret = self.__class__.__name__ + ":\n" - ret += indent_text(self.first.__repr__(), 4) + '\n' - sym = '' + ret += indent_text(self.first.__repr__(), 4) + "\n" + sym = "" if self._operator_symbol is not None: - sym += self._operator_symbol + ' ' + sym += self._operator_symbol + " " sec_text = indent_text(self.second.__repr__(), 2) - sec_text = sym + sec_text[len(sym):] - ret += indent_text(sec_text, 4) + '\n' + sec_text = sym + sec_text[len(sym) :] + ret += indent_text(sec_text, 4) + "\n" ret = ret.rstrip() while ret[-1] == "\n" and ret[-2] == "\n": ret = ret[:-1] return ret - def get_data_specs(self): - specs = list(self.first.get_data_specs()) + \ - list(self.second.get_data_specs()) + def get_data_specs(self) -> Tuple[DataSpec, ...]: + specs = list(self.first.get_data_specs()) + list(self.second.get_data_specs()) return combine_data_specs(*specs) class ZipSweeps(CombineSweeps): + _operator_symbol = "*" - _operator_symbol = '*' - - def __iter__(self): - for fd, sd in zip(self.first, self.second): + def __iter__(self) -> Iterator[Dict[str, Any]]: + for fd, sd in zip(iter(self.first), iter(self.second)): ret = fd.copy() ret.update(sd) yield ret class AppendSweeps(CombineSweeps): + _operator_symbol = "+" - _operator_symbol = '+' - - def __iter__(self): - for ret in itertools.chain(self.first, self.second): + def __iter__(self) -> Iterator[Dict[str, Any]]: + ret: Dict[str, Any] + for ret in itertools.chain(iter(self.first), iter(self.second)): yield ret class NestSweeps(CombineSweeps): + _operator_symbol = "@" - _operator_symbol = '@' - - def __iter__(self): - for outer in self.first: - for inner in self.second: + def __iter__(self) -> Iterator[Dict[str, Any]]: + for outer in iter(self.first): + for inner in iter(self.second): ret = outer.copy() ret.update(inner) yield ret @@ -557,17 +592,17 @@ class AsyncRecord: wrapped_setup: Callable - def __init__(self, *specs): + def __init__(self, *specs: Any) -> None: self.specs = specs - self.communicator = {} + self.communicator: Dict[str, Any] = {} - def __call__(self, fun) -> Callable: + def __call__(self, fun: Callable) -> Callable: """ When the decorator is called the experiment function gets wrapped so that it returns an Sweep object composed of 2 different Sweeps, the setup sweep and the collector Sweep. """ - def sweep(collector_options={}, **setup_kwargs) -> Sweep: + def sweep(collector_options: Dict[str, Any] = {}, **setup_kwargs: Any) -> Sweep: """ Returns a Sweep comprised of 2 different Sweeps: start_sweep and collector_sweep. start_sweep should perform any setup actions as well as starting the actual experiment. This sweep is only @@ -577,7 +612,9 @@ def sweep(collector_options={}, **setup_kwargs) -> Sweep: :param collector_kwargs: Any arguments that the collector needs. """ start_sweep = once(self.wrap_setup(fun)) - collector_sweep = Sweep(as_pointer(self.collect, *self.specs).using(**collector_options)) + collector_sweep = Sweep( + as_pointer(self.collect, *self.specs).using(**collector_options) + ) ret = start_sweep + collector_sweep ret.set_options(**{fun.__name__: setup_kwargs}) return ret @@ -599,8 +636,10 @@ def wrap_setup(self, fun: Callable, *args: Any, **kwargs: Any) -> Callable: update_wrapper(self.wrapped_setup, fun) return self.wrapped_setup - def setup(self, fun, *args, **kwargs): + def setup(self, fun: Callable, *args: Any, **kwargs: Any) -> Any: return fun(*args, **kwargs) - def collect(self, *args, **kwargs) -> Generator[Dict, None, None]: + def collect( + self, *args: Any, **kwargs: Any + ) -> Generator[Dict[str, Any], None, None]: yield {} diff --git a/src/labcore/protocols/__init__.py b/src/labcore/protocols/__init__.py new file mode 100644 index 0000000..50ceb59 --- /dev/null +++ b/src/labcore/protocols/__init__.py @@ -0,0 +1,30 @@ +from labcore.protocols.base import ( + PLATFORMTYPE as PLATFORMTYPE, +) +from labcore.protocols.base import ( + BranchBase as BranchBase, +) +from labcore.protocols.base import ( + Condition as Condition, +) +from labcore.protocols.base import ( + OperationStatus as OperationStatus, +) +from labcore.protocols.base import ( + ParamImprovement as ParamImprovement, +) +from labcore.protocols.base import ( + PlatformTypes as PlatformTypes, +) +from labcore.protocols.base import ( + ProtocolBase as ProtocolBase, +) +from labcore.protocols.base import ( + ProtocolOperation as ProtocolOperation, +) +from labcore.protocols.base import ( + ProtocolParameterBase as ProtocolParameterBase, +) +from labcore.protocols.base import ( + SuperOperationBase as SuperOperationBase, +) diff --git a/src/labcore/protocols/base.py b/src/labcore/protocols/base.py new file mode 100644 index 0000000..44fdefe --- /dev/null +++ b/src/labcore/protocols/base.py @@ -0,0 +1,1144 @@ +from __future__ import annotations + +import base64 +import logging +from dataclasses import dataclass +from enum import Enum, auto +from pathlib import Path +from typing import Any, Callable + +import markdown +import numpy as np +from numpy.typing import ArrayLike + +logger = logging.getLogger(__name__) + + +def serialize_fit_params(params: Any) -> dict[str, dict[str, float | None]]: + return {n: dict(value=v.value, error=v.stderr) for n, v in params.items()} + + +class PlatformTypes(Enum): + OPX = auto() + QICK = auto() + DUMMY = auto() + + +PLATFORMTYPE: PlatformTypes | None = None + + +@dataclass +class ProtocolParameterBase: + """ + Base class for protocol parameters with platform-specific getter/setter methods. + + Subclasses must implement the platform-specific getter/setter methods: + - _qick_getter() / _qick_setter(value) for QICK platform + - _opx_getter() / _opx_setter(value) for OPX platform + - _dummy_getter() / _dummy_setter(value) for DUMMY platform + + These methods should directly call the QCoDeS-style parameter with: + - Get: my_param() + - Set: my_param(value) + + Example: + >>> @dataclass + >>> class QubitFrequency(ProtocolParameterBase): + ... name: str = field(default="Qubit IF", init=False) + ... description: str = field(default="Qubit intermediate frequency", init=False) + ... + ... def _qick_getter(self): + ... return self.params.qubit.f_ge() + ... + ... def _qick_setter(self, value): + ... return self.params.qubit.f_ge(value) + ... + ... def _opx_getter(self): + ... return self.params.qubit.frequency() + ... + ... def _opx_setter(self, value): + ... return self.params.qubit.frequency(value) + """ + + global PLATFORMTYPE + + name: str + # FIXME: this should be typed as ProxyInstrumentModule from instrumentserver, + # but labcore should not depend on that package. params is only stored and + # passed through here; hardware access lives in subclasses in the measurement + # repo. Retype to a structural Protocol or remove the dependency when cleaning + # up the migration. + params: Any + description: str + platform_type: PlatformTypes | None = PLATFORMTYPE + + # dataclasses defaults are evaluated at import time, not runtime. + # This means we need to re-apply the PLATFORMTYPE when an instance is created + def __post_init__(self) -> None: + if self.platform_type is None: + self.platform_type = PLATFORMTYPE + + # Validate params is provided for non-DUMMY platforms + if self.platform_type != PlatformTypes.DUMMY and self.params is None: + raise ValueError( + f"params argument is required for {self.platform_type} platform" + ) + + def __call__(self, value: Any = None) -> Any: + """ + QCoDeS-style parameter calling convention. + + Usage: + param() # Get value (no arguments) + param(42) # Set value to 42 + """ + if value is None: + # Getter: no arguments provided + match self.platform_type: + case PlatformTypes.QICK: + return self._qick_getter() + case PlatformTypes.OPX: + return self._opx_getter() + case PlatformTypes.DUMMY: + return self._dummy_getter() + raise NotImplementedError( + f"Platform type {self.platform_type} not implemented" + ) + else: + # Setter: value provided + match self.platform_type: + case PlatformTypes.QICK: + return self._qick_setter(value) + case PlatformTypes.OPX: + return self._opx_setter(value) + case PlatformTypes.DUMMY: + return self._dummy_setter(value) + raise NotImplementedError( + f"Platform type {self.platform_type} not implemented" + ) + + def _qick_getter(self) -> Any: + """Get parameter value for QICK platform. Subclasses must implement.""" + raise NotImplementedError( + f"QICK getter not implemented for parameter '{self.name}'" + ) + + def _qick_setter(self, value: Any) -> None: + """Set parameter value for QICK platform. Subclasses must implement.""" + raise NotImplementedError( + f"QICK setter not implemented for parameter '{self.name}'" + ) + + def _opx_getter(self) -> Any: + """Get parameter value for OPX platform. Subclasses must implement.""" + raise NotImplementedError( + f"OPX getter not implemented for parameter '{self.name}'" + ) + + def _opx_setter(self, value: Any) -> None: + """Set parameter value for OPX platform. Subclasses must implement.""" + raise NotImplementedError( + f"OPX setter not implemented for parameter '{self.name}'" + ) + + def _dummy_getter(self) -> Any: + """Get parameter value for DUMMY platform. Subclasses must implement.""" + raise NotImplementedError( + f"DUMMY getter not implemented for parameter '{self.name}'" + ) + + def _dummy_setter(self, value: Any) -> None: + """Set parameter value for DUMMY platform. Subclasses must implement.""" + raise NotImplementedError( + f"DUMMY setter not implemented for parameter '{self.name}'" + ) + + +class OperationStatus(Enum): + """ + Return status for ProtocolOperation.evaluate() + + Indicates what the protocol executor should do next with this operation. + """ + + SUCCESS = "success" + RETRY = "retry" + FAILURE = "failure" + + def __str__(self) -> str: + return self.value + + def __repr__(self) -> str: + return f"OperationStatus.{self.name}" + + +@dataclass +class ParamImprovement: + old_value: Any + new_value: Any + param: ProtocolParameterBase + + +# TODO: How do we handle different saving for different scenarios? For example: +# For the lab we use run_measurement, for something like the lccf it will be something different. +# In the same way that if some other lab wants to run this, they might want to automatically save other stuff. +class ProtocolOperation: + """ """ + + DEFAULT_MAX_ATTEMPTS = 3 # Default max retry attempts for operations + + def __init__(self) -> None: + global PLATFORMTYPE + + self.name = self.__class__.__name__ + + self.platform_type: PlatformTypes | None = PLATFORMTYPE + self.data_loc: Path | None = None + + self.input_params: dict[str, ProtocolParameterBase] = {} + self.output_params: dict[str, ProtocolParameterBase] = {} + + # Specifies in plain english what is the condition for the measurement to be successful + self.condition: str = "" + self.report_output: list[str | Path] = [] + + self.independents: dict[str, ArrayLike] = {} + self.dependents: dict[str, ArrayLike] = {} + + self.figure_paths: list[Path] = [] + + self.improvements: list[ParamImprovement] = [] + + # Retry/attempt tracking + self.max_attempts: int = self.DEFAULT_MAX_ATTEMPTS + self.current_attempt: int = 0 + self.total_attempts_made: int = 0 + + def _register_inputs(self, **kwargs: ProtocolParameterBase) -> None: + """Register input parameters as both attributes and in the dictionary""" + for name, param in kwargs.items(): + setattr(self, name, param) + self.input_params[name] = param + + def _register_outputs(self, **kwargs: ProtocolParameterBase) -> None: + """Register output parameters as both attributes and in the dictionary""" + for name, param in kwargs.items(): + setattr(self, name, param) + self.output_params[name] = param + + def _measure_qick(self) -> Path: + raise NotImplementedError("QICK measurement not implemented") + + def _measure_opx(self) -> Path: + raise NotImplementedError("OPX measurement not implemented") + + def _measure_dummy(self) -> Path: + raise NotImplementedError("DUMMY measurement not implemented") + + # TODO: How do we verify directionality in the information with datasets that are 2D or bigger. + # e.i.: How do we know which independent is store in which axis? This needs to be standardized for each measurement + def _verify_shape(self) -> bool: + # Check if any arrays in independents are empty + for name, array in self.independents.items(): + arr = np.asarray(array) + if arr.size == 0: + print(f"independents['{name}'] is empty") + return False + + # Check if any arrays in dependents are empty + for name, array in self.dependents.items(): + arr = np.asarray(array) + if arr.size == 0: + print(f"dependents['{name}'] is empty") + return False + + all_arrays = {} + all_arrays.update(self.independents) + all_arrays.update(self.dependents) + + if not all_arrays: + return True + + shapes = {} + for name, array in all_arrays.items(): + arr = np.asarray(array) + shapes[name] = arr.shape + + first_shape = next(iter(shapes.values())) + + if all(shape == first_shape for shape in shapes.values()): + return True + else: + print("Shape mismatch detected:") + for name, shape in shapes.items(): + print(f" {name}: {shape}") + return False + + def measure(self) -> Path: + match self.platform_type: + case PlatformTypes.QICK: + loc = self._measure_qick() + self.data_loc = loc + return loc + case PlatformTypes.OPX: + loc = self._measure_opx() + self.data_loc = loc + return loc + case PlatformTypes.DUMMY: + loc = self._measure_dummy() + self.data_loc = loc + return loc + raise NotImplementedError(f"Platform type {self.platform_type} not implemented") + + def analyze(self) -> None: + raise NotImplementedError("Analyze method not implemented") + + def _load_data_opx(self) -> None: + raise NotImplementedError("Load OPX data method not implemented") + + def _load_data_qick(self) -> None: + raise NotImplementedError("Load QICK data method not implemented") + + def _load_data_dummy(self) -> None: + raise NotImplementedError("Load DUMMY data method not implemented") + + def load_data(self) -> bool: + match self.platform_type: + case PlatformTypes.QICK: + self._load_data_qick() + case PlatformTypes.OPX: + self._load_data_opx() + case PlatformTypes.DUMMY: + self._load_data_dummy() + case _: + raise NotImplementedError( + f"Platform type {self.platform_type} not implemented" + ) + + return self._verify_shape() + + def evaluate(self) -> OperationStatus: + """ + Evaluate operation results and recommend next action. + + Subclasses must implement custom logic based on their domain knowledge. + + Returns: + OperationStatus.SUCCESS: Proceed to next operation + OperationStatus.RETRY: Retry this operation (if attempts remain) + OperationStatus.FAILURE: Stop protocol execution + """ + raise NotImplementedError("Subclasses must implement evaluate()") + + def execute(self) -> OperationStatus: + """ + Execute the full operation workflow: measure -> load_data -> analyze -> evaluate. + + This method increments attempt counters and adds repetition headers to reports. + + Returns: + OperationStatus from evaluate() method + """ + # Increment attempt counter + self.current_attempt += 1 + self.total_attempts_made += 1 + + # Add repetition header to report if this is a retry + if self.current_attempt > 1: + repetition_header = f"### ATTEMPT {self.current_attempt}\n\n" + self.report_output.append(repetition_header) + + # Execute the four-step workflow + self.measure() + self.load_data() + self.analyze() + status = self.evaluate() + + return status + + +class SuperOperationBase(ProtocolOperation): + """ + A composite operation that groups multiple operations together. + + SuperOperations execute a sequence of operations as a single unit, + sharing the same retry mechanism. If any sub-operation fails, the + entire SuperOperation can be retried. + + Key features: + - All sub-operations execute in sequence + - Retry logic applies to the entire group + - Reports are aggregated under the SuperOperation section + - Branching (Conditions) is NOT permitted in SuperOperations + - Protocol treats it the same as a regular operation + + Subclasses must: + 1. Call super().__init__() in their __init__ + 2. Set self.operations to a list of ProtocolOperation instances + 3. NOT include any Condition instances in self.operations + + Example: + >>> class CalibrationSuite(SuperOperationBase): + ... def __init__(self, params): + ... super().__init__() + ... self.operations = [ + ... ResonatorSpectroscopy(params), + ... PowerRabi(params), + ... PiSpectroscopy(params) + ... ] + ... + ... def evaluate(self) -> OperationStatus: + ... # All operations succeeded, check aggregate quality + ... if all_calibrations_good(): + ... return OperationStatus.SUCCESS + ... else: + ... return OperationStatus.RETRY + """ + + def __init__(self) -> None: + super().__init__() + self.operations: list[ProtocolOperation] = [] + + def _validate_operations(self) -> None: + """Validate that operations list contains only ProtocolOperation instances""" + + for i, op in enumerate(self.operations): + if isinstance(op, Condition): + raise ValueError( + f"SuperOperation '{self.name}' contains a Condition at index {i}. " + f"Branching is not permitted in SuperOperations. " + f"Use regular branches in the protocol instead." + ) + if not isinstance(op, ProtocolOperation): + raise TypeError( + f"SuperOperation '{self.name}' contains invalid item at index {i}: {type(op)}. " + f"Only ProtocolOperation instances are allowed." + ) + + def execute(self) -> OperationStatus: + """ + Execute all sub-operations in sequence and aggregate their reports. + + This method overrides ProtocolOperation.execute() to iterate through + all sub-operations instead of calling measure/load_data/analyze. + + Returns: + OperationStatus from the evaluate() method + """ + # Validate operations before executing + self._validate_operations() + + # Increment attempt counter + self.current_attempt += 1 + self.total_attempts_made += 1 + + # Add retry header if needed + if self.current_attempt > 1: + repetition_header = f"### ATTEMPT {self.current_attempt}\n\n" + self.report_output.append(repetition_header) + + # Add SuperOperation header to report + header = f"## {self.name}\n\n" + self.report_output.append(header) + + # Execute each sub-operation + for i, op in enumerate(self.operations): + logger.info( + f" [{self.name}] Executing sub-operation {i + 1}/{len(self.operations)}: {op.name}" + ) + + # Execute the operation (measure -> load_data -> analyze -> evaluate) + try: + status = op.execute() + except Exception as e: + logger.error( + f" [{self.name}] Exception in sub-operation {op.name}: {e}" + ) + # If a sub-operation fails, the SuperOperation fails + return OperationStatus.FAILURE + + # Aggregate the sub-operation's report output + if op.report_output: + # Add sub-operation section header + self.report_output.append(f"### {op.name}\n\n") + # Add all report items from the sub-operation + self.report_output.extend(op.report_output) + self.report_output.append("\n") + + # Check sub-operation status + if status == OperationStatus.FAILURE: + logger.error( + f" [{self.name}] Sub-operation {op.name} failed critically" + ) + return OperationStatus.FAILURE + elif status == OperationStatus.RETRY: + logger.warning( + f" [{self.name}] Sub-operation {op.name} requested retry" + ) + # Don't immediately fail - let evaluate() decide + # But we could track this for evaluation logic + elif status == OperationStatus.SUCCESS: + logger.info(f" [{self.name}] Sub-operation {op.name} succeeded") + + # Aggregate figure paths + if hasattr(op, "figure_paths"): + self.figure_paths.extend(op.figure_paths) + + # Aggregate improvements + if hasattr(op, "improvements"): + self.improvements.extend(op.improvements) + + # Call the subclass's evaluate() method to determine overall status + status = self.evaluate() + + return status + + def measure(self) -> Path: + """Not used in SuperOperation - operations handle their own measurement""" + raise NotImplementedError( + f"measure() does not make sense for a SuperOperation. " + f"Sub-operations in '{self.name}' handle their own measurement." + ) + + def load_data(self) -> bool: + """Not used in SuperOperation - operations handle their own data loading""" + raise NotImplementedError( + f"load_data() does not make sense for a SuperOperation. " + f"Sub-operations in '{self.name}' handle their own data loading." + ) + + def analyze(self) -> None: + """Not used in SuperOperation - operations handle their own analysis""" + raise NotImplementedError( + f"analyze() does not make sense for a SuperOperation. " + f"Sub-operations in '{self.name}' handle their own analysis." + ) + + +class ProtocolBase: + def __init__(self, report_path: Path = Path("")): + + self.name = self.__class__.__name__ + self.root_branch: BranchBase | None = None # Required - must be set by subclass + # True for successful protocol execution, False for failure at some operation, None for un-ran protocol + self.success: bool | None = None + + self.report_path = report_path + + # Don't run if the user didn't select a platform + if PLATFORMTYPE is None: + raise ValueError("Please choose a platform") + + def _flatten_branch_for_execution( + self, branch: BranchBase + ) -> list[ProtocolOperation | Condition]: + """ + Recursively flatten a branch into a list of operations and conditions. + + Does NOT evaluate conditions - just collects them for runtime evaluation. + Returns a list containing both ProtocolOperation and Condition instances. + """ + items: list[ProtocolOperation | Condition] = [] + + for item in branch.items: + if isinstance(item, ProtocolOperation): + items.append(item) + elif isinstance(item, Condition): + # Add the condition itself (will be evaluated during execution) + items.append(item) + # Don't traverse into branches yet - will be done at runtime + + return items + + def _collect_all_operations_from_branch( + self, branch: BranchBase + ) -> list[ProtocolOperation]: + """ + Recursively collect ALL operations from a branch tree (for parameter verification). + + Includes operations from all branches, not just taken ones. + Also collects operations from inside SuperOperations. + """ + + operations: list[ProtocolOperation] = [] + + for item in branch.items: + if isinstance(item, SuperOperationBase): + # Add the SuperOperation itself + operations.append(item) + # Also collect all sub-operations from the SuperOperation + for sub_op in item.operations: + operations.append(sub_op) + elif isinstance(item, ProtocolOperation): + operations.append(item) + elif isinstance(item, Condition): + # Collect from BOTH branches + operations.extend( + self._collect_all_operations_from_branch(item.true_branch) + ) + operations.extend( + self._collect_all_operations_from_branch(item.false_branch) + ) + + return operations + + def verify_all_parameters(self) -> bool: + """Verify parameters in all operations across all branches""" + + if self.root_branch is None: + raise ValueError( + f"Protocol {self.name} must set self.root_branch in __init__" + ) + + all_ops = self._collect_all_operations_from_branch(self.root_branch) + + failures = {} + for op in all_ops: + for param_name, param in op.input_params.items(): + try: + param() # Use callable syntax to verify parameter access + except Exception as e: + failures[param.name] = e + + for param_name, param in op.output_params.items(): + try: + param() # Use callable syntax to verify parameter access + except Exception as e: + failures[param.name] = e + + if failures: + f_list = [f"{str(k)}: {str(v)}" for k, v in failures.items()] + msg = f"The following parameters could not be verified: {f_list}" + raise AttributeError(msg) + + return True + + def _assemble_report(self) -> Path: + """Generate HTML report from executed operations and conditions with embedded images""" + # Create report directory structure + report_dir = self.report_path / f"{self.name}_report" + report_dir.mkdir(exist_ok=True) + + # Use the executed_items collected during execution + if not hasattr(self, "executed_items"): + logger.warning("No executed items found for report") + executed_items = [] + else: + executed_items = self.executed_items + + # Build table of contents and sections + toc_entries = [] + sections = [] + + for idx, item in enumerate(executed_items): + section_id = f"section-{idx}" + + if isinstance(item, Condition): + # Create section for condition + section_title = f"Condition: {item.name}" + toc_entries.append((section_id, section_title, "condition")) + + # Build section content + section_content = [] + if item.report_output: + for report_item in item.report_output: + section_content.append(f"{report_item}\n") + + sections.append( + { + "id": section_id, + "title": section_title, + "type": "condition", + "content": "\n".join(section_content), + } + ) + + elif isinstance(item, ProtocolOperation): + # Create section for operation + section_title = f"Operation: {item.name}" + toc_entries.append((section_id, section_title, "operation")) + + # Build section content + section_content = [] + if item.report_output: + for op_item in item.report_output: + if isinstance(op_item, Path): + # Embed image as base64 data URI + try: + with open(op_item, "rb") as img_file: + img_data = img_file.read() + img_base64 = base64.b64encode(img_data).decode( + "utf-8" + ) + + # Determine MIME type from file extension + ext = op_item.suffix.lower() + mime_type = { + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".svg": "image/svg+xml", + ".webp": "image/webp", + }.get(ext, "image/png") + + # Add as markdown image with data URI + section_content.append( + f"![Figure](data:{mime_type};base64,{img_base64})\n" + ) + except Exception as e: + logger.warning(f"Failed to embed image {op_item}: {e}") + section_content.append( + f"![Figure - Error loading image]({op_item.name})\n" + ) + else: + section_content.append(f"{op_item}\n") + + sections.append( + { + "id": section_id, + "title": section_title, + "type": "operation", + "content": "\n".join(section_content), + } + ) + + # Build HTML with table of contents and sections + html_parts = [] + + # Add title + html_parts.append(f"

{self.name} Report

\n") + + # Add table of contents + html_parts.append("

Table of Contents

\n") + html_parts.append("
    \n") + for section_id, title, item_type in toc_entries: + css_class = "toc-condition" if item_type == "condition" else "toc-operation" + html_parts.append( + f"
  • {title}
  • \n" + ) + html_parts.append("
\n") + html_parts.append("
\n") + + # Add sections + for section in sections: + html_parts.append( + f"
\n" + ) + html_parts.append( + f"

\n" + ) + html_parts.append( + f" {section['title']}\n" + ) + html_parts.append("

\n") + + # Wrappable content div + html_parts.append( + f"
\n" + ) + + # Convert markdown content to HTML + section_html = markdown.markdown( + section["content"], extensions=["extra", "codehilite"] + ) + html_parts.append(section_html) + + html_parts.append("↑ Back to top\n") + html_parts.append("
\n") # Close section-content + html_parts.append("
\n") + html_parts.append("
\n") + + html_body = "\n".join(html_parts) + + # Create complete HTML with enhanced CSS + html_template = f""" + + + + + {self.name} Report + + + + +{html_body} + + +""" + + # Save HTML file + html_file = report_dir / f"{self.name}_report.html" + html_file.write_text(html_template) + + logger.info(f"Report generated: {html_file}") + + return html_file + + def _execute_operation(self, op: ProtocolOperation) -> bool: + """ + Execute a single operation with retry logic. + + The operation's execute() method handles the workflow, this method handles retries. + + Returns: + True if operation succeeded, False if failed + """ + max_attempts = op.max_attempts + + # Reset attempt counter for this operation + op.current_attempt = 0 + + while op.current_attempt < max_attempts: + # Execute operation (it will increment current_attempt internally) + try: + status = op.execute() + except Exception as e: + logger.error(f" Exception during {op.name}: {e}") + return False + + # Handle status + if status == OperationStatus.SUCCESS: + logger.info(f" SUCCESS: {op.name} succeeded") + return True + + elif status == OperationStatus.RETRY: + if op.current_attempt < max_attempts: + logger.warning( + f" RETRY: {op.name} requesting retry (attempt {op.current_attempt}/{max_attempts})" + ) + continue # Retry + else: + logger.error( + f" FAILURE: {op.name} exhausted {max_attempts} attempts" + ) + return False + + elif status == OperationStatus.FAILURE: + logger.error(f" FAILURE: {op.name} failed critically") + return False + + else: + logger.error(f" Unknown status: {status}") + return False + + # Should not reach here + return False + + def _execute_branch( + self, branch: BranchBase + ) -> tuple[list[ProtocolOperation | Condition], bool]: + """ + Recursively execute a branch, evaluating conditions at runtime. + + Returns list of executed items (operations and conditions) for reporting. + """ + + executed_items: list[ProtocolOperation | Condition] = [] + + for item in branch.items: + if isinstance(item, ProtocolOperation): + logger.info(f" Executing: {item.name}") + success = self._execute_operation(item) + executed_items.append(item) + + if not success: + return executed_items, False + + elif isinstance(item, Condition): + taken_branch = item.evaluate() + executed_items.append(item) + + sub_items, success = self._execute_branch(taken_branch) + executed_items.extend(sub_items) + + if not success: + return executed_items, False + + return executed_items, True + + def execute(self) -> None: + """Execute protocol by recursively executing branches""" + logger.info(f"Starting protocol: {self.name}") + + if self.root_branch is None: + raise ValueError( + f"Protocol {self.name} must set self.root_branch in __init__" + ) + + # Verify all parameters before starting + if not self.verify_all_parameters(): + logger.error("Parameter verification failed") + self.success = False + return + + # Execute the root branch (conditions evaluated at runtime) + logger.info(f"Executing protocol with root branch: '{self.root_branch.name}'") + self.executed_items, success = self._execute_branch(self.root_branch) + + if not success: + logger.error("Protocol stopped due to operation failure") + self.success = False + else: + logger.info("Protocol completed successfully") + self.success = True + + self._assemble_report() + + +class BranchBase: + """ + A named sequence of operations that can be executed. + + Branches can contain: + - Operations (ProtocolOperation instances) + - Conditions (conditional routing to other branches) + + Args: + name: Display name for this branch + + Example: + >>> main_branch = BranchBase("MainCalibration") + >>> main_branch.append(ResonatorSpectroscopy(params)) + >>> main_branch.append(SaturationSpectroscopy(params)) + """ + + def __init__(self, name: str = "Branch"): + self.name = name + self.items: list = [] # Will contain ProtocolOperation or Condition instances + + def append(self, item: ProtocolOperation | Condition) -> BranchBase: + """Add an operation or condition to this branch""" + self.items.append(item) + return self # Allow chaining + + def extend(self, items: list[ProtocolOperation | Condition]) -> BranchBase: + """Add multiple operations/conditions to this branch""" + self.items.extend(items) + return self # Allow chaining + + def __repr__(self) -> str: + return f"BranchBase(name='{self.name}', items={len(self.items)})" + + +class Condition: + """ + A conditional decision point that routes execution to different branches. + + During protocol execution, the condition is evaluated and either + true_branch or false_branch is executed. + + Args: + condition: Callable returning bool to determine which branch + true_branch: Branch to execute if condition is True + false_branch: Branch to execute if condition is False + name: Optional name for this condition (for logging/reporting) + + Example: + >>> res_snr = params['resonator']['snr'] + >>> + >>> high_snr_branch = BranchBase("HighSNR") + >>> high_snr_branch.append(PiSpectroscopy(params)) + >>> + >>> low_snr_branch = BranchBase("LowSNR") + >>> low_snr_branch.append(PowerRabi(params)) + >>> low_snr_branch.append(PiSpectroscopy(params)) + >>> + >>> condition = Condition( + >>> condition=lambda: res_snr() > 5.0, + >>> true_branch=high_snr_branch, + >>> false_branch=low_snr_branch, + >>> name="SNR Check" + >>> ) + """ + + def __init__( + self, + condition: Callable[[], bool], + true_branch: BranchBase, + false_branch: BranchBase, + name: str = "Condition", + ): + self.condition_func = condition + self.true_branch = true_branch + self.false_branch = false_branch + self.name = name + + # Runtime state + self.condition_result: bool | None = None + self.taken_branch: BranchBase | None = None + self.report_output: list[str] = [] + + def evaluate(self) -> BranchBase: + """Evaluate condition and return the branch to execute""" + logger.info(f"[Condition] Evaluating condition: '{self.name}'") + logger.debug( + f"[Condition] Available branches: TRUE → '{self.true_branch.name}', FALSE → '{self.false_branch.name}'" + ) + + self.condition_result = self.condition_func() + logger.info(f"[Condition] '{self.name}' evaluated to: {self.condition_result}") + + if self.condition_result: + logger.info( + f"[Condition] '{self.name}': TRUE → executing branch '{self.true_branch.name}'" + ) + self.taken_branch = self.true_branch + message = f"**Condition '{self.name}' has the result {self.condition_result} (TRUE branch). Choosing branch '{self.true_branch.name}'**" + self.report_output.append(message) + return self.true_branch + else: + logger.info( + f"[Condition] '{self.name}': FALSE → executing branch '{self.false_branch.name}'" + ) + self.taken_branch = self.false_branch + message = f"**Condition '{self.name}' has the result {self.condition_result} (FALSE branch). Choosing branch '{self.false_branch.name}'**" + self.report_output.append(message) + return self.false_branch + + def __repr__(self) -> str: + return f"Condition(name='{self.name}')" diff --git a/src/labcore/scripts/__init__.py b/src/labcore/scripts/__init__.py new file mode 100644 index 0000000..7178541 --- /dev/null +++ b/src/labcore/scripts/__init__.py @@ -0,0 +1,5 @@ +"""Command-line scripts for labcore. + +These modules are not intended for direct import but are exposed as CLI tools +via entry points defined in pyproject.toml. +""" diff --git a/src/labcore/scripts/monitr_server.py b/src/labcore/scripts/monitr_server.py new file mode 100644 index 0000000..c4ee3dc --- /dev/null +++ b/src/labcore/scripts/monitr_server.py @@ -0,0 +1,60 @@ +import argparse +import logging +from pathlib import Path +from typing import Any, Union + +import panel as pn + +from labcore.analysis.hvapps import DataSelect, DDH5LoaderNode + +pn.extension() # noqa: E402 + +logger = logging.getLogger(__file__) + + +def make_template(data_root: Union[str, Path] = ".") -> Any: + ds = DataSelect(data_root) + loader = DDH5LoaderNode() + + def data_selected_cb(*events: Any) -> None: + loader.file_path = events[0].new + + ds.param.watch(data_selected_cb, ["selected_path"]) + + def refilter_data_select(*events: Any) -> None: + ds.data_select() + + ds.param.watch(refilter_data_select, ["search_term"]) + + temp = pn.template.BootstrapTemplate( + site="labcore", title="autoplot", sidebar=[], main=[ds, loader] + ) + + return temp + + +def run_autoplot() -> None: + parser = argparse.ArgumentParser( + description="Data monitoring program made for Pfaff lab by Rocky Daehler, building" + " on Plottr made by Wolfgang Pfaff. Run command on it's own to start the" + " application, and pass an (optional) path to the data directory as a" + " second argument." + ) + parser.add_argument("Datapath", nargs="?", default=".") + + args = parser.parse_args() + + data_root = Path(args.Datapath) + if not data_root.is_dir(): + logger.error( + "Provided Path was invalid.\nPlease provide a path to an existing directory housing your data." + ) + return + + logger.info(f"Running Labcore.Autoplot on data from {data_root}") + + template = make_template(data_root) + template.show() + + +make_template(".").servable() diff --git a/scripts/reconstruct_safe_write_data.py b/src/labcore/scripts/reconstruct_safe_write_data.py similarity index 51% rename from scripts/reconstruct_safe_write_data.py rename to src/labcore/scripts/reconstruct_safe_write_data.py index 318cf08..12ad718 100644 --- a/scripts/reconstruct_safe_write_data.py +++ b/src/labcore/scripts/reconstruct_safe_write_data.py @@ -3,28 +3,33 @@ Meant to be a backup way of reconstructing data if something goes wrong. """ -import logging import argparse +import logging from pathlib import Path -from labcore.data.datadict_storage import reconstruct_safe_write_data, datadict_to_hdf5 - +from labcore.data.datadict_storage import datadict_to_hdf5, reconstruct_safe_write_data logger = logging.getLogger(__name__) -def main(): - parser = argparse.ArgumentParser(description='Reconstructing the safe write data') - - parser.add_argument("path", - help="path to directory containing a .tmp folder. .tmp doesn't have to be in the path", - default=None) - parser.add_argument("--filename", - help="Name for the newly created ddh5 file. Defaults to data.ddh5", - default="data.ddh5") - parser.add_argument("--file_timeout", - help="time before a timeout error is raised when interacting with files, in seconds", - default=None) +def main() -> None: + parser = argparse.ArgumentParser(description="Reconstructing the safe write data") + + parser.add_argument( + "path", + help="path to directory containing a .tmp folder. .tmp doesn't have to be in the path", + default=None, + ) + parser.add_argument( + "--filename", + help="Name for the newly created ddh5 file. Defaults to data.ddh5", + default="data.ddh5", + ) + parser.add_argument( + "--file_timeout", + help="time before a timeout error is raised when interacting with files, in seconds", + default=None, + ) args = parser.parse_args() @@ -41,14 +46,12 @@ def main(): if not path.exists(): raise FileNotFoundError(f"No .tmp folder found in {path}") if ddh5_path.exists(): - raise FileExistsError(f"File {ddh5_path} already exists. Remove it or change filename before continuing.") + raise FileExistsError( + f"File {ddh5_path} already exists. Remove it or change filename before continuing." + ) dd = reconstruct_safe_write_data(path, file_timeout=file_timeout) - + datadict_to_hdf5(dd, ddh5_path) logger.info(f"Reconstruction of safe write data in {path} completed.") - - - - diff --git a/labcore/testing/__init__.py b/src/labcore/testing/__init__.py similarity index 100% rename from labcore/testing/__init__.py rename to src/labcore/testing/__init__.py diff --git a/labcore/testing/dispersive_qubit_readout_data.py b/src/labcore/testing/dispersive_qubit_readout_data.py similarity index 72% rename from labcore/testing/dispersive_qubit_readout_data.py rename to src/labcore/testing/dispersive_qubit_readout_data.py index 1059529..9c9d149 100644 --- a/labcore/testing/dispersive_qubit_readout_data.py +++ b/src/labcore/testing/dispersive_qubit_readout_data.py @@ -21,19 +21,19 @@ """ from typing import Union -import numpy as np -from ..data.datadict import str2dd +import numpy as np +from ..data.datadict import DataDict, str2dd -angle = np.pi/2 -amp = 2. +angle = np.pi / 2 +amp = 2.0 noise = 0.5 def gs_probability(theta: Union[np.ndarray, float]) -> Union[np.ndarray, float]: """Compute ground state probability for given rotation angle.""" - return np.cos(theta/2.)**2. + return np.cos(theta / 2.0) ** 2.0 def state_data(state: np.ndarray) -> np.ndarray: @@ -42,8 +42,9 @@ def state_data(state: np.ndarray) -> np.ndarray: :param state: array of states (0 or 1, typically). :returns: array of complex readout results.""" mean = amp * np.exp(1j * state * angle) - return np.random.normal(loc=mean.real, scale=noise, size=state.shape) + \ - 1j * np.random.normal(loc=mean.imag, scale=noise, size=state.shape) + return np.random.normal( + loc=mean.real, scale=noise, size=state.shape + ) + 1j * np.random.normal(loc=mean.imag, scale=noise, size=state.shape) def angle_data(theta: float, n: int = 100) -> np.ndarray: @@ -57,7 +58,7 @@ def angle_data(theta: float, n: int = 100) -> np.ndarray: state = rng.choice( np.array([0, 1]), size=n, - p=np.array([gs_probability(theta), 1-gs_probability(theta)]), + p=np.array([gs_probability(theta), 1 - gs_probability(theta)]), ) return state_data(state) @@ -73,37 +74,42 @@ def probability_data(p_e: float, n: int = 100) -> np.ndarray: state = rng.choice( np.array([0, 1]), size=n, - p=np.array([1-p_e, p_e]), + p=np.array([1 - p_e, p_e]), ) return state_data(state) -def rabi(Omega_0, Delta, t): - Omega = (Omega_0**2 + Delta**2)**.5 - return (Omega_0 / Omega)**2 * (1.0-np.cos(Omega*t))/2. +def rabi(Omega_0: float, Delta: float, t: float) -> float: + Omega = (Omega_0**2 + Delta**2) ** 0.5 + return (Omega_0 / Omega) ** 2 * (1.0 - np.cos(Omega * t)) / 2.0 -def chevron_dataset(Omega_0, Delta_vals, t_vals, n): +def chevron_dataset( + Omega_0: float, Delta_vals: np.ndarray, t_vals: np.ndarray, n: int +) -> DataDict: data = str2dd("signal(repetition, detuning, time); detuning[Hz]; time[s];") for i, Delta in enumerate(Delta_vals): for j, t in enumerate(t_vals): data.add_data( - signal=probability_data(rabi(2*np.pi*Omega_0, 2*np.pi*Delta, t), n=n), - repetition=np.arange(n, dtype=int)+1, + signal=probability_data( + rabi(2 * np.pi * Omega_0, 2 * np.pi * Delta, t), n=n + ), + repetition=np.arange(n, dtype=int) + 1, detuning=Delta, time=t, ) return data - -if __name__ == '__main__': +# TODO: Remove the script from this file +if __name__ == "__main__": from matplotlib import pyplot as plt - data = angle_data(np.pi/2., n=1000) + data = angle_data(np.pi / 2.0, n=1000) extent = np.abs(data).max() - hist, xe, ye = np.histogram2d(data.real, data.imag, - bins=list(np.linspace(-extent, extent, 51))) + hist, xe, ye = np.histogram2d( + data.real, data.imag, bins=list(np.linspace(-extent, extent, 51)) + ) fig, ax = plt.subplots(1, 1) im = ax.pcolormesh(xe, ye, hist.T) cb = fig.colorbar(im) diff --git a/src/labcore/testing/protocol_dummy/__init__.py b/src/labcore/testing/protocol_dummy/__init__.py new file mode 100644 index 0000000..e8a0fa0 --- /dev/null +++ b/src/labcore/testing/protocol_dummy/__init__.py @@ -0,0 +1,83 @@ +from labcore.testing.protocol_dummy.cosine import CosineOperation as CosineOperation +from labcore.testing.protocol_dummy.dummy_protocol import ( + DummyProtocol as DummyProtocol, +) +from labcore.testing.protocol_dummy.dummy_protocol import ( + DummySuperOperation as DummySuperOperation, +) +from labcore.testing.protocol_dummy.exponential import ( + ExponentialOperation as ExponentialOperation, +) +from labcore.testing.protocol_dummy.exponential_decay import ( + ExponentialDecayOperation as ExponentialDecayOperation, +) +from labcore.testing.protocol_dummy.exponentially_decaying_sine import ( + ExponentiallyDecayingSineOperation as ExponentiallyDecayingSineOperation, +) +from labcore.testing.protocol_dummy.gaussian import ( + GaussianOperation as GaussianOperation, +) +from labcore.testing.protocol_dummy.linear import LinearOperation as LinearOperation +from labcore.testing.protocol_dummy.parameters import ( + CosineAmplitude as CosineAmplitude, +) +from labcore.testing.protocol_dummy.parameters import ( + CosineFrequency as CosineFrequency, +) +from labcore.testing.protocol_dummy.parameters import ( + CosineOffset as CosineOffset, +) +from labcore.testing.protocol_dummy.parameters import ( + CosinePhase as CosinePhase, +) +from labcore.testing.protocol_dummy.parameters import ( + ExponentialA as ExponentialA, +) +from labcore.testing.protocol_dummy.parameters import ( + ExponentialB as ExponentialB, +) +from labcore.testing.protocol_dummy.parameters import ( + ExponentialDecayAmplitude as ExponentialDecayAmplitude, +) +from labcore.testing.protocol_dummy.parameters import ( + ExponentialDecayOffset as ExponentialDecayOffset, +) +from labcore.testing.protocol_dummy.parameters import ( + ExponentialDecayTau as ExponentialDecayTau, +) +from labcore.testing.protocol_dummy.parameters import ( + ExponentiallyDecayingSineAmplitude as ExponentiallyDecayingSineAmplitude, +) +from labcore.testing.protocol_dummy.parameters import ( + ExponentiallyDecayingSineFrequency as ExponentiallyDecayingSineFrequency, +) +from labcore.testing.protocol_dummy.parameters import ( + ExponentiallyDecayingSineOffset as ExponentiallyDecayingSineOffset, +) +from labcore.testing.protocol_dummy.parameters import ( + ExponentiallyDecayingSinePhase as ExponentiallyDecayingSinePhase, +) +from labcore.testing.protocol_dummy.parameters import ( + ExponentiallyDecayingSineTau as ExponentiallyDecayingSineTau, +) +from labcore.testing.protocol_dummy.parameters import ( + GaussianAmplitude as GaussianAmplitude, +) +from labcore.testing.protocol_dummy.parameters import ( + GaussianCenter as GaussianCenter, +) +from labcore.testing.protocol_dummy.parameters import ( + GaussianOffset as GaussianOffset, +) +from labcore.testing.protocol_dummy.parameters import ( + GaussianSigma as GaussianSigma, +) +from labcore.testing.protocol_dummy.parameters import ( + LinearOffset as LinearOffset, +) +from labcore.testing.protocol_dummy.parameters import ( + LinearSlope as LinearSlope, +) +from labcore.testing.protocol_dummy.parameters import ( + _DummyParameterBase as _DummyParameterBase, +) diff --git a/src/labcore/testing/protocol_dummy/cosine.py b/src/labcore/testing/protocol_dummy/cosine.py new file mode 100644 index 0000000..14d97eb --- /dev/null +++ b/src/labcore/testing/protocol_dummy/cosine.py @@ -0,0 +1,186 @@ +import logging +from pathlib import Path +from typing import Any, cast + +import matplotlib.pyplot as plt +import numpy as np + +from labcore.analysis import DatasetAnalysis +from labcore.analysis.fit import FitResult +from labcore.analysis.fitfuncs.generic import Cosine +from labcore.data.datadict_storage import datadict_from_hdf5 +from labcore.measurement import Sweep +from labcore.measurement.record import dependent, independent, recording +from labcore.measurement.storage import run_and_save_sweep +from labcore.protocols.base import OperationStatus, ParamImprovement, ProtocolOperation +from labcore.testing.protocol_dummy.parameters import ( + CosineAmplitude, + CosineFrequency, + CosineOffset, + CosinePhase, +) + +plt.switch_backend("agg") + +logger = logging.getLogger(__name__) + + +class CosineOperation(ProtocolOperation): + SNR_THRESHOLD = 2 + + def __init__(self, params: Any = None) -> None: + super().__init__() + + self.frequency: CosineFrequency + self.phase: CosinePhase + self.offset: CosineOffset + self._register_inputs( + frequency=CosineFrequency(params), + phase=CosinePhase(params), + offset=CosineOffset(params), + ) + self.amplitude: CosineAmplitude + self._register_outputs(amplitude=CosineAmplitude(params)) + + self.condition = f"Success if the SNR of the Cosine fit is bigger than the current threshold of {self.SNR_THRESHOLD}" + + self.independents = {"x_values": []} + self.dependents = {"y_values": []} + + self.fit_result: FitResult | None = None + self.snr: float | None = None + + def _measure_dummy(self) -> Path: + """ + Creates fake data that looks like a Cosine with noise using a sweep. + Model: A * cos(2*pi*f*x + phi) + of + """ + logger.info("Starting Cosine measurement (generating fake Cosine data)") + + # True Cosine parameters + true_amplitude = 5.0 + true_frequency = 0.2 + true_phase = np.pi / 4 + true_offset = 2.0 + + # Create x values for the sweep + x_values = np.linspace(0, 20, 100) + + @recording(independent("x"), dependent("y")) + def measure_cosine(x_val: float) -> tuple[float, float]: + """Generate a single Cosine data point with noise""" + y_clean = ( + true_amplitude * np.cos(2 * np.pi * true_frequency * x_val + true_phase) + + true_offset + ) + noise = np.random.normal(0, 0.3) + return x_val, y_clean + noise + + sweep = Sweep(x_values, measure_cosine) + + # Run and save the sweep + logger.debug("Sweep created, running measurement") + loc, data_array = run_and_save_sweep(sweep, "data", self.name) + logger.info(f"Measurement complete, data saved to {loc}") + + return Path(loc) + + def _load_data_dummy(self) -> None: + """Load the generated fake data""" + assert self.data_loc is not None + path = self.data_loc / "data.ddh5" + if not path.exists(): + raise FileNotFoundError(f"File {path} does not exist") + data = datadict_from_hdf5(path) + + self.independents["x_values"] = data["x"]["values"] + self.dependents["y_values"] = data["y"]["values"] + + def analyze(self) -> None: + """Fit the data to a Cosine""" + assert self.data_loc is not None + with DatasetAnalysis(self.data_loc, self.name) as ds: + x = np.asarray(self.independents["x_values"]) + y = np.asarray(self.dependents["y_values"]) + + # Perform Cosine fit + fit = Cosine(x, y) + self.fit_result = cast(FitResult, fit.run()) + fit_curve = self.fit_result.eval() + residuals = y - fit_curve + + # Calculate SNR + amplitude = self.fit_result.params["A"].value + noise = np.std(residuals) + snr = float(np.abs(amplitude / (4 * noise))) + self.snr = snr + + # Create plot + fig, ax = plt.subplots() + ax.set_title("Cosine - Amplitude Fit") + ax.set_xlabel("X Values (A.U)") + ax.set_ylabel("Y Values (A.U)") + ax.plot(x, y, "o", label="Data", markersize=4) + ax.plot(x, fit_curve, "-", label="Cosine Fit", linewidth=2) + ax.legend() + ax.grid(True, alpha=0.3) + + # Save results + ds.add(fit_curve=fit_curve, fit_result=self.fit_result, snr=snr) + ds.add_figure(self.name, fig=fig) + + image_path = ds._new_file_path(ds.savefolders[1], self.name, suffix="png") + self.figure_paths.append(image_path) + + def evaluate(self) -> OperationStatus: + """ + Evaluate if the fit was successful based on SNR threshold. + If successful, update the amplitude output parameter with the fitted amplitude value. + """ + header = ( + f"## Cosine - Amplitude Fit\n" + f"Generated fake Cosine data and fitted it to extract amplitude.\n" + f"Data Path: `{self.data_loc}`\n" + f"Plot:\n" + ) + plot_image = self.figure_paths[0].resolve() + + assert self.snr is not None + assert self.fit_result is not None + if self.snr >= self.SNR_THRESHOLD: + logger.info( + f"SNR of {self.snr} is bigger than threshold of {self.SNR_THRESHOLD}. Applying new values" + ) + + old_value = self.amplitude() + new_value = self.fit_result.params["A"].value + + logger.info( + f"Updating {self.amplitude.name} from {old_value} to {new_value}" + ) + self.amplitude(new_value) + + self.improvements = [ParamImprovement(old_value, new_value, self.amplitude)] + + msg_2 = ( + f"Fit was **SUCCESSFUL** with an SNR of {self.snr:.3f}.\n" + f"{self.amplitude.name} updated: {old_value} -> {new_value:.3f}\n\n" + f"**Fit Report:**\n```\n{str(self.fit_result.lmfit_result.fit_report())}\n```\n\n" + ) + + self.report_output = [header, plot_image, msg_2] + + return OperationStatus.SUCCESS + + logger.info( + f"SNR of {self.snr} is smaller than threshold of {self.SNR_THRESHOLD}. Evaluation failed" + ) + + msg_2 = ( + f"Fit was **UNSUCCESSFUL** with an SNR of {self.snr:.3f}.\n" + f"NO value has been changed.\n" + f"Fit Report:\n\n```\n{str(self.fit_result.lmfit_result.fit_report())}\n```\n" + ) + self.report_output = [header, plot_image, msg_2] + + return OperationStatus.FAILURE diff --git a/src/labcore/testing/protocol_dummy/dummy_protocol.py b/src/labcore/testing/protocol_dummy/dummy_protocol.py new file mode 100644 index 0000000..7a74bec --- /dev/null +++ b/src/labcore/testing/protocol_dummy/dummy_protocol.py @@ -0,0 +1,119 @@ +import logging +from pathlib import Path +from typing import Any + +from labcore.protocols.base import ( + BranchBase, + Condition, + OperationStatus, + ProtocolBase, + SuperOperationBase, +) +from labcore.testing.protocol_dummy.cosine import CosineOperation +from labcore.testing.protocol_dummy.exponential import ExponentialOperation +from labcore.testing.protocol_dummy.exponential_decay import ExponentialDecayOperation +from labcore.testing.protocol_dummy.exponentially_decaying_sine import ( + ExponentiallyDecayingSineOperation, +) +from labcore.testing.protocol_dummy.gaussian import GaussianOperation +from labcore.testing.protocol_dummy.linear import LinearOperation + +logger = logging.getLogger(__name__) + +# Global test variable - change this to switch branches +USE_BRANCH_A = True +USE_BRANCH_C = True + + +class DummySuperOperation(SuperOperationBase): + """ + Example SuperOperation that groups multiple calibration operations together. + + This demonstrates: + - Grouping Exponential and ExponentialDecay operations + - Retry mechanism (will retry 2 times for testing) + - Report aggregation + """ + + def __init__(self, params: Any = None) -> None: + super().__init__() + + # Define the sequence of operations + self.operations = [ + ExponentialOperation(params), + ExponentialDecayOperation(params), + ] + + # Configure retry behavior + self.max_attempts = 3 # Will retry up to 3 times total + + def evaluate(self) -> OperationStatus: + """ + Evaluate the overall success of all sub-operations. + Uses same retry testing mechanism as GaussianProtocol. + """ + logger.info( + f"[{self.name}] All sub-operations completed at attempt {self.total_attempts_made}" + ) + + # Similar retry mechanism to GaussianProtocol for testing + if self.total_attempts_made != 3: + logger.info( + f"[{self.name}] At {self.total_attempts_made} attempts, requesting retry for testing" + ) + return OperationStatus.RETRY + + logger.info(f"[{self.name}] Reached 3 attempts, returning SUCCESS") + return OperationStatus.SUCCESS + + +class DummyProtocol(ProtocolBase): + def __init__(self, params: Any = None, report_path: Path = Path("")) -> None: + super().__init__(report_path) + + # Create main branch + main = BranchBase("Main") + main.append(GaussianOperation(params)) + + # Add SuperOperation to demonstrate grouping operations with retry + main.append(DummySuperOperation(params)) + + # Branch A: Cosine, Exponential, Linear + branch_a = BranchBase("BranchA") + branch_a.append(CosineOperation(params)) + branch_a.append(ExponentialOperation(params)) + branch_a.append(LinearOperation(params)) + + # Branch B: ExponentialDecay, ExponentiallyDecayingSine + branch_b = BranchBase("BranchB") + branch_b.append(ExponentialDecayOperation(params)) + branch_b.append(ExponentiallyDecayingSineOperation(params)) + + branch_c = BranchBase("BranchC") + branch_c.append(LinearOperation(params)) + branch_c.append(GaussianOperation(params)) + + branch_d = BranchBase("BranchD") + branch_d.append(ExponentialOperation(params)) + branch_d.append(CosineOperation(params)) + + branch_b.append( + Condition( + condition=lambda: USE_BRANCH_C, + true_branch=branch_c, + false_branch=branch_d, + name="Sub-Branch Selection", + ) + ) + + # Add conditional branching + main.append( + Condition( + condition=lambda: USE_BRANCH_A, + true_branch=branch_a, + false_branch=branch_b, + name="Branch Selection", + ) + ) + + self.root_branch = main diff --git a/src/labcore/testing/protocol_dummy/exponential.py b/src/labcore/testing/protocol_dummy/exponential.py new file mode 100644 index 0000000..8fe1dfc --- /dev/null +++ b/src/labcore/testing/protocol_dummy/exponential.py @@ -0,0 +1,174 @@ +import logging +from pathlib import Path +from typing import Any, cast + +import matplotlib.pyplot as plt +import numpy as np + +from labcore.analysis import DatasetAnalysis +from labcore.analysis.fit import FitResult +from labcore.analysis.fitfuncs.generic import Exponential +from labcore.data.datadict_storage import datadict_from_hdf5 +from labcore.measurement.record import dependent, independent, recording +from labcore.measurement.storage import run_and_save_sweep +from labcore.measurement.sweep import Sweep +from labcore.protocols.base import OperationStatus, ParamImprovement, ProtocolOperation +from labcore.testing.protocol_dummy.parameters import ExponentialA, ExponentialB + +plt.switch_backend("agg") + +logger = logging.getLogger(__name__) + + +class ExponentialOperation(ProtocolOperation): + SNR_THRESHOLD = 2 + + def __init__(self, params: Any = None) -> None: + super().__init__() + + self.b: ExponentialB + self._register_inputs(b=ExponentialB(params)) + self.a: ExponentialA + self._register_outputs(a=ExponentialA(params)) + + self.condition = f"Success if the SNR of the Exponential fit is bigger than the current threshold of {self.SNR_THRESHOLD}" + + self.independents = {"x_values": []} + self.dependents = {"y_values": []} + + self.fit_result: FitResult | None = None + self.snr: float | None = None + + def _measure_dummy(self) -> Path: + """ + Creates fake data that looks like an Exponential with noise using a sweep. + Model: a * b^x + """ + logger.info( + "Starting Exponential measurement (generating fake Exponential data)" + ) + + # True Exponential parameters + true_a = 1 + true_b = 2 + + # Create x values for the sweep (shorter range to avoid overflow) + x_values = np.linspace(0, 10, 50) + + # Define a measurement function that generates Exponential data with noise + @recording(independent("x"), dependent("y")) + def measure_exponential(x_val: float) -> tuple[float, float]: + """Generate a single Exponential data point with noise""" + y_clean = true_a * (true_b**x_val) + # Noise proportional to signal magnitude (5% relative noise) + noise = np.random.normal(0, 0.01 * y_clean) + return x_val, y_clean + noise + + # Create the sweep using Sweep directly + sweep = Sweep(x_values, measure_exponential) + + # Run and save the sweep + logger.debug("Sweep created, running measurement") + loc, data_array = run_and_save_sweep(sweep, "data", self.name) + logger.info(f"Measurement complete, data saved to {loc}") + + return Path(loc) + + def _load_data_dummy(self) -> None: + """Load the generated fake data""" + assert self.data_loc is not None + path = self.data_loc / "data.ddh5" + if not path.exists(): + raise FileNotFoundError(f"File {path} does not exist") + data = datadict_from_hdf5(path) + + self.independents["x_values"] = data["x"]["values"] + self.dependents["y_values"] = data["y"]["values"] + + def analyze(self) -> None: + """Fit the data to an Exponential""" + assert self.data_loc is not None + with DatasetAnalysis(self.data_loc, self.name) as ds: + x = np.asarray(self.independents["x_values"]) + y = np.asarray(self.dependents["y_values"]) + + # Perform Exponential fit + fit = Exponential(x, y) + self.fit_result = cast(FitResult, fit.run()) + fit_curve = self.fit_result.eval() + residuals = y - fit_curve + + # Calculate SNR + # For exponential, use relative noise (residuals/signal) to avoid bias from growth + relative_residuals = residuals / fit_curve + relative_noise = np.std(relative_residuals) + snr = float(np.abs(1 / (4 * relative_noise))) + self.snr = snr + + # Create plot + fig, ax = plt.subplots() + ax.set_title("Exponential - Coefficient Fit") + ax.set_xlabel("X Values (A.U)") + ax.set_ylabel("Y Values (A.U)") + ax.plot(x, y, "o", label="Data", markersize=4) + ax.plot(x, fit_curve, "-", label="Exponential Fit", linewidth=2) + ax.legend() + ax.grid(True, alpha=0.3) + + # Save results + ds.add(fit_curve=fit_curve, fit_result=self.fit_result, snr=snr) + ds.add_figure(self.name, fig=fig) + + image_path = ds._new_file_path(ds.savefolders[1], self.name, suffix="png") + self.figure_paths.append(image_path) + + def evaluate(self) -> OperationStatus: + """ + Evaluate if the fit was successful based on SNR threshold. + If successful, update the 'a' output parameter with the fitted coefficient value. + """ + header = ( + f"## Exponential - Coefficient Fit\n" + f"Generated fake Exponential data and fitted it to extract coefficient.\n" + f"Data Path: `{self.data_loc}`\n" + f"Plot:\n" + ) + plot_image = self.figure_paths[0].resolve() + + assert self.snr is not None + assert self.fit_result is not None + if self.snr >= self.SNR_THRESHOLD: + logger.info( + f"SNR of {self.snr} is bigger than threshold of {self.SNR_THRESHOLD}. Applying new values" + ) + + old_value = self.a() + new_value = self.fit_result.params["a"].value + + logger.info(f"Updating {self.a.name} from {old_value} to {new_value}") + self.a(new_value) + + self.improvements = [ParamImprovement(old_value, new_value, self.a)] + + msg_2 = ( + f"Fit was **SUCCESSFUL** with an SNR of {self.snr:.3f}.\n" + f"{self.a.name} updated: {old_value} -> {new_value:.3f}\n\n" + f"**Fit Report:**\n```\n{str(self.fit_result.lmfit_result.fit_report())}\n```\n\n" + ) + + self.report_output = [header, plot_image, msg_2] + + return OperationStatus.SUCCESS + + logger.info( + f"SNR of {self.snr} is smaller than threshold of {self.SNR_THRESHOLD}. Evaluation failed" + ) + + msg_2 = ( + f"Fit was **UNSUCCESSFUL** with an SNR of {self.snr:.3f}.\n" + f"NO value has been changed.\n" + f"Fit Report:\n\n```\n{str(self.fit_result.lmfit_result.fit_report())}\n```\n" + ) + self.report_output = [header, plot_image, msg_2] + + return OperationStatus.FAILURE diff --git a/src/labcore/testing/protocol_dummy/exponential_decay.py b/src/labcore/testing/protocol_dummy/exponential_decay.py new file mode 100644 index 0000000..2aeccb2 --- /dev/null +++ b/src/labcore/testing/protocol_dummy/exponential_decay.py @@ -0,0 +1,182 @@ +import logging +from pathlib import Path +from typing import Any, cast + +import matplotlib.pyplot as plt +import numpy as np + +from labcore.analysis import DatasetAnalysis +from labcore.analysis.fit import FitResult +from labcore.analysis.fitfuncs.generic import ExponentialDecay +from labcore.data.datadict_storage import datadict_from_hdf5 +from labcore.measurement.record import dependent, independent, recording +from labcore.measurement.storage import run_and_save_sweep +from labcore.measurement.sweep import Sweep +from labcore.protocols.base import OperationStatus, ParamImprovement, ProtocolOperation +from labcore.testing.protocol_dummy.parameters import ( + ExponentialDecayAmplitude, + ExponentialDecayOffset, + ExponentialDecayTau, +) + +plt.switch_backend("agg") + +logger = logging.getLogger(__name__) + + +class ExponentialDecayOperation(ProtocolOperation): + SNR_THRESHOLD = 2 + + def __init__(self, params: Any = None) -> None: + super().__init__() + + self.offset: ExponentialDecayOffset + self.tau: ExponentialDecayTau + self._register_inputs( + offset=ExponentialDecayOffset(params), tau=ExponentialDecayTau(params) + ) + self.amplitude: ExponentialDecayAmplitude + self._register_outputs(amplitude=ExponentialDecayAmplitude(params)) + + self.condition = f"Success if the SNR of the Exponential Decay fit is bigger than the current threshold of {self.SNR_THRESHOLD}" + + self.independents = {"x_values": []} + self.dependents = {"y_values": []} + + self.fit_result: FitResult | None = None + self.snr: float | None = None + + def _measure_dummy(self) -> Path: + """ + Creates fake data that looks like an Exponential Decay with noise using a sweep. + Model: A * exp(-x/tau) + of + """ + logger.info( + "Starting Exponential Decay measurement (generating fake Exponential Decay data)" + ) + + # True Exponential Decay parameters + true_amplitude = 8.0 + true_offset = 1.0 + true_tau = 3.0 + + # Create x values for the sweep + x_values = np.linspace(0, 15, 75) + + # Define a measurement function that generates Exponential Decay data with noise + @recording(independent("x"), dependent("y")) + def measure_exponential_decay(x_val: float) -> tuple[float, float]: + """Generate a single Exponential Decay data point with noise""" + y_clean = true_amplitude * np.exp(-x_val / true_tau) + true_offset + noise = np.random.normal(0, 0.3) + return x_val, y_clean + noise + + # Create the sweep using Sweep directly + sweep = Sweep(x_values, measure_exponential_decay) + + # Run and save the sweep + logger.debug("Sweep created, running measurement") + loc, data_array = run_and_save_sweep(sweep, "data", self.name) + logger.info(f"Measurement complete, data saved to {loc}") + + return Path(loc) + + def _load_data_dummy(self) -> None: + """Load the generated fake data""" + assert self.data_loc is not None + path = self.data_loc / "data.ddh5" + if not path.exists(): + raise FileNotFoundError(f"File {path} does not exist") + data = datadict_from_hdf5(path) + + self.independents["x_values"] = data["x"]["values"] + self.dependents["y_values"] = data["y"]["values"] + + def analyze(self) -> None: + """Fit the data to an Exponential Decay""" + assert self.data_loc is not None + with DatasetAnalysis(self.data_loc, self.name) as ds: + x = np.asarray(self.independents["x_values"]) + y = np.asarray(self.dependents["y_values"]) + + # Perform Exponential Decay fit + fit = ExponentialDecay(x, y) + self.fit_result = cast(FitResult, fit.run()) + fit_curve = self.fit_result.eval() + residuals = y - fit_curve + + # Calculate SNR + amplitude = self.fit_result.params["A"].value + noise = np.std(residuals) + snr = float(np.abs(amplitude / (4 * noise))) + self.snr = snr + + # Create plot + fig, ax = plt.subplots() + ax.set_title("Exponential Decay - Amplitude Fit") + ax.set_xlabel("X Values (A.U)") + ax.set_ylabel("Y Values (A.U)") + ax.plot(x, y, "o", label="Data", markersize=4) + ax.plot(x, fit_curve, "-", label="Exponential Decay Fit", linewidth=2) + ax.legend() + ax.grid(True, alpha=0.3) + + # Save results + ds.add(fit_curve=fit_curve, fit_result=self.fit_result, snr=snr) + ds.add_figure(self.name, fig=fig) + + image_path = ds._new_file_path(ds.savefolders[1], self.name, suffix="png") + self.figure_paths.append(image_path) + + def evaluate(self) -> OperationStatus: + """ + Evaluate if the fit was successful based on SNR threshold. + If successful, update the amplitude output parameter with the fitted amplitude value. + """ + header = ( + f"## Exponential Decay - Amplitude Fit\n" + f"Generated fake Exponential Decay data and fitted it to extract amplitude.\n" + f"Data Path: `{self.data_loc}`\n" + f"Plot:\n" + ) + plot_image = self.figure_paths[0].resolve() + + assert self.snr is not None + assert self.fit_result is not None + if self.snr >= self.SNR_THRESHOLD: + logger.info( + f"SNR of {self.snr} is bigger than threshold of {self.SNR_THRESHOLD}. Applying new values" + ) + + old_value = self.amplitude() + new_value = self.fit_result.params["A"].value + + logger.info( + f"Updating {self.amplitude.name} from {old_value} to {new_value}" + ) + self.amplitude(new_value) + + self.improvements = [ParamImprovement(old_value, new_value, self.amplitude)] + + msg_2 = ( + f"Fit was **SUCCESSFUL** with an SNR of {self.snr:.3f}.\n" + f"{self.amplitude.name} updated: {old_value} -> {new_value:.3f}\n\n" + f"**Fit Report:**\n```\n{str(self.fit_result.lmfit_result.fit_report())}\n```\n\n" + ) + + self.report_output = [header, plot_image, msg_2] + + return OperationStatus.SUCCESS + + logger.info( + f"SNR of {self.snr} is smaller than threshold of {self.SNR_THRESHOLD}. Evaluation failed" + ) + + msg_2 = ( + f"Fit was **UNSUCCESSFUL** with an SNR of {self.snr:.3f}.\n" + f"NO value has been changed.\n" + f"Fit Report:\n\n```\n{str(self.fit_result.lmfit_result.fit_report())}\n```\n" + ) + self.report_output = [header, plot_image, msg_2] + + return OperationStatus.FAILURE diff --git a/src/labcore/testing/protocol_dummy/exponentially_decaying_sine.py b/src/labcore/testing/protocol_dummy/exponentially_decaying_sine.py new file mode 100644 index 0000000..9180ea2 --- /dev/null +++ b/src/labcore/testing/protocol_dummy/exponentially_decaying_sine.py @@ -0,0 +1,198 @@ +import logging +from pathlib import Path +from typing import Any, cast + +import matplotlib.pyplot as plt +import numpy as np + +from labcore.analysis import DatasetAnalysis +from labcore.analysis.fit import FitResult +from labcore.analysis.fitfuncs.generic import ExponentiallyDecayingSine +from labcore.data.datadict_storage import datadict_from_hdf5 +from labcore.measurement.record import dependent, independent, recording +from labcore.measurement.storage import run_and_save_sweep +from labcore.measurement.sweep import Sweep +from labcore.protocols.base import OperationStatus, ParamImprovement, ProtocolOperation +from labcore.testing.protocol_dummy.parameters import ( + ExponentiallyDecayingSineAmplitude, + ExponentiallyDecayingSineFrequency, + ExponentiallyDecayingSineOffset, + ExponentiallyDecayingSinePhase, + ExponentiallyDecayingSineTau, +) + +plt.switch_backend("agg") + +logger = logging.getLogger(__name__) + + +class ExponentiallyDecayingSineOperation(ProtocolOperation): + SNR_THRESHOLD = 2 + + def __init__(self, params: Any = None) -> None: + super().__init__() + + self.offset: ExponentiallyDecayingSineOffset + self.frequency: ExponentiallyDecayingSineFrequency + self.phase: ExponentiallyDecayingSinePhase + self.tau: ExponentiallyDecayingSineTau + self._register_inputs( + offset=ExponentiallyDecayingSineOffset(params), + frequency=ExponentiallyDecayingSineFrequency(params), + phase=ExponentiallyDecayingSinePhase(params), + tau=ExponentiallyDecayingSineTau(params), + ) + self.amplitude: ExponentiallyDecayingSineAmplitude + self._register_outputs(amplitude=ExponentiallyDecayingSineAmplitude(params)) + + self.condition = f"Success if the SNR of the Exponentially Decaying Sine fit is bigger than the current threshold of {self.SNR_THRESHOLD}" + + self.independents = {"x_values": []} + self.dependents = {"y_values": []} + + self.fit_result: FitResult | None = None + self.snr: float | None = None + + def _measure_dummy(self) -> Path: + """ + Creates fake data that looks like an Exponentially Decaying Sine with noise using a sweep. + Model: A * sin(2*pi*(f*x + phi/360)) * exp(-x/tau) + of + """ + logger.info( + "Starting Exponentially Decaying Sine measurement (generating fake data)" + ) + + # True Exponentially Decaying Sine parameters + true_amplitude = 6.0 + true_offset = 1.0 + true_frequency = 0.3 + true_phase = 45.0 # in degrees + true_tau = 5.0 + + # Create x values for the sweep + x_values = np.linspace(0, 20, 100) + + # Define a measurement function that generates Exponentially Decaying Sine data with noise + @recording(independent("x"), dependent("y")) + def measure_exp_decay_sine(x_val: float) -> tuple[float, float]: + """Generate a single Exponentially Decaying Sine data point with noise""" + y_clean = ( + true_amplitude + * np.sin(2 * np.pi * (true_frequency * x_val + true_phase / 360)) + * np.exp(-x_val / true_tau) + + true_offset + ) + noise = np.random.normal(0, 0.2) + return x_val, y_clean + noise + + # Create the sweep using Sweep directly + sweep = Sweep(x_values, measure_exp_decay_sine) + + # Run and save the sweep + logger.debug("Sweep created, running measurement") + loc, data_array = run_and_save_sweep(sweep, "data", self.name) + logger.info(f"Measurement complete, data saved to {loc}") + + return Path(loc) + + def _load_data_dummy(self) -> None: + """Load the generated fake data""" + assert self.data_loc is not None + path = self.data_loc / "data.ddh5" + if not path.exists(): + raise FileNotFoundError(f"File {path} does not exist") + data = datadict_from_hdf5(path) + + self.independents["x_values"] = data["x"]["values"] + self.dependents["y_values"] = data["y"]["values"] + + def analyze(self) -> None: + """Fit the data to an Exponentially Decaying Sine""" + assert self.data_loc is not None + with DatasetAnalysis(self.data_loc, self.name) as ds: + x = np.asarray(self.independents["x_values"]) + y = np.asarray(self.dependents["y_values"]) + + # Perform Exponentially Decaying Sine fit + fit = ExponentiallyDecayingSine(x, y) + self.fit_result = cast(FitResult, fit.run()) + fit_curve = self.fit_result.eval() + residuals = y - fit_curve + + # Calculate SNR + amplitude = self.fit_result.params["A"].value + noise = np.std(residuals) + snr = float(np.abs(amplitude / (4 * noise))) + self.snr = snr + + # Create plot + fig, ax = plt.subplots() + ax.set_title("Exponentially Decaying Sine - Amplitude Fit") + ax.set_xlabel("X Values (A.U)") + ax.set_ylabel("Y Values (A.U)") + ax.plot(x, y, "o", label="Data", markersize=4) + ax.plot( + x, fit_curve, "-", label="Exponentially Decaying Sine Fit", linewidth=2 + ) + ax.legend() + ax.grid(True, alpha=0.3) + + # Save results + ds.add(fit_curve=fit_curve, fit_result=self.fit_result, snr=snr) + ds.add_figure(self.name, fig=fig) + + image_path = ds._new_file_path(ds.savefolders[1], self.name, suffix="png") + self.figure_paths.append(image_path) + + def evaluate(self) -> OperationStatus: + """ + Evaluate if the fit was successful based on SNR threshold. + If successful, update the amplitude output parameter with the fitted amplitude value. + """ + header = ( + f"## Exponentially Decaying Sine - Amplitude Fit\n" + f"Generated fake Exponentially Decaying Sine data and fitted it to extract amplitude.\n" + f"Data Path: `{self.data_loc}`\n" + f"Plot:\n" + ) + plot_image = self.figure_paths[0].resolve() + + assert self.snr is not None + assert self.fit_result is not None + if self.snr >= self.SNR_THRESHOLD: + logger.info( + f"SNR of {self.snr} is bigger than threshold of {self.SNR_THRESHOLD}. Applying new values" + ) + + old_value = self.amplitude() + new_value = self.fit_result.params["A"].value + + logger.info( + f"Updating {self.amplitude.name} from {old_value} to {new_value}" + ) + self.amplitude(new_value) + + self.improvements = [ParamImprovement(old_value, new_value, self.amplitude)] + + msg_2 = ( + f"Fit was **SUCCESSFUL** with an SNR of {self.snr:.3f}.\n" + f"{self.amplitude.name} updated: {old_value} -> {new_value:.3f}\n\n" + f"**Fit Report:**\n```\n{str(self.fit_result.lmfit_result.fit_report())}\n```\n\n" + ) + + self.report_output = [header, plot_image, msg_2] + + return OperationStatus.SUCCESS + + logger.info( + f"SNR of {self.snr} is smaller than threshold of {self.SNR_THRESHOLD}. Evaluation failed" + ) + + msg_2 = ( + f"Fit was **UNSUCCESSFUL** with an SNR of {self.snr:.3f}.\n" + f"NO value has been changed.\n" + f"Fit Report:\n\n```\n{str(self.fit_result.lmfit_result.fit_report())}\n```\n" + ) + self.report_output = [header, plot_image, msg_2] + + return OperationStatus.FAILURE diff --git a/src/labcore/testing/protocol_dummy/gaussian.py b/src/labcore/testing/protocol_dummy/gaussian.py new file mode 100644 index 0000000..ce871ba --- /dev/null +++ b/src/labcore/testing/protocol_dummy/gaussian.py @@ -0,0 +1,191 @@ +import logging +from pathlib import Path +from typing import Any, cast + +import matplotlib.pyplot as plt +import numpy as np + +from labcore.analysis import DatasetAnalysis +from labcore.analysis.fit import FitResult +from labcore.analysis.fitfuncs.generic import Gaussian +from labcore.data.datadict_storage import datadict_from_hdf5 +from labcore.measurement.record import dependent, independent, recording +from labcore.measurement.storage import run_and_save_sweep +from labcore.measurement.sweep import Sweep +from labcore.protocols.base import OperationStatus, ParamImprovement, ProtocolOperation +from labcore.testing.protocol_dummy.parameters import ( + GaussianAmplitude, + GaussianCenter, + GaussianOffset, + GaussianSigma, +) + +plt.switch_backend("agg") + +logger = logging.getLogger(__name__) + + +class GaussianOperation(ProtocolOperation): + SNR_THRESHOLD = 2 + + def __init__(self, params: Any = None) -> None: + super().__init__() + + self.center: GaussianCenter + self.sigma: GaussianSigma + self.offset: GaussianOffset + self._register_inputs( + center=GaussianCenter(params), + sigma=GaussianSigma(params), + offset=GaussianOffset(params), + ) + self.amplitude: GaussianAmplitude + self._register_outputs(amplitude=GaussianAmplitude(params)) + + self.condition = f"Success if the SNR of the Gaussian fit is bigger than the current threshold of {self.SNR_THRESHOLD}" + + self.independents = {"x_values": []} + self.dependents = {"y_values": []} + + self.fit_result: FitResult | None = None + self.snr: float | None = None + + def _measure_dummy(self) -> Path: + """ + Creates fake data that looks like a Gaussian with noise using a sweep. + Uses input parameters (center, sigma, offset) to generate the Gaussian. + """ + logger.info("Starting Gaussian measurement (generating fake Gaussian data)") + + # Gaussian parameters: amplitude, center, width + true_amplitude = 10.0 + true_center = 0.5 + true_sigma = 2.0 + + # Create x values for the sweep + x_values = np.linspace(-10, 10, 100) + + @recording(independent("x"), dependent("y")) + def measure_gaussian(x_val: float) -> tuple[float, float]: + """Generate a single Gaussian data point with noise""" + y_clean = true_amplitude * np.exp( + -((x_val - true_center) ** 2) / (2 * true_sigma**2) + ) + noise = np.random.normal(0, 0.5) + return x_val, y_clean + noise + + sweep = Sweep(x_values, measure_gaussian) + + # Run and save the sweep + logger.debug("Sweep created, running measurement") + loc, data_array = run_and_save_sweep(sweep, "data", self.name) + logger.info(f"Measurement complete, data saved to {loc}") + + return Path(loc) + + def _load_data_dummy(self) -> None: + """Load the generated fake data""" + assert self.data_loc is not None + path = self.data_loc / "data.ddh5" + if not path.exists(): + raise FileNotFoundError(f"File {path} does not exist") + data = datadict_from_hdf5(path) + + self.independents["x_values"] = data["x"]["values"] + self.dependents["y_values"] = data["y"]["values"] + + def analyze(self) -> None: + """Fit the data to a Gaussian""" + assert self.data_loc is not None + with DatasetAnalysis(self.data_loc, self.name) as ds: + x = np.asarray(self.independents["x_values"]) + y = np.asarray(self.dependents["y_values"]) + + # Perform Gaussian fit + fit = Gaussian(x, y) + self.fit_result = cast(FitResult, fit.run()) + fit_curve = self.fit_result.eval() + residuals = y - fit_curve + + # Calculate SNR + amplitude = self.fit_result.params["A"].value + noise = np.std(residuals) + snr = float(np.abs(amplitude / (4 * noise))) + self.snr = snr + + # Create plot + fig, ax = plt.subplots() + ax.set_title("Gaussian - Amplitude Fit") + ax.set_xlabel("X Values (A.U)") + ax.set_ylabel("Y Values (A.U)") + ax.plot(x, y, "o", label="Data", markersize=4) + ax.plot(x, fit_curve, "-", label="Gaussian Fit", linewidth=2) + ax.legend() + ax.grid(True, alpha=0.3) + + # Save results + ds.add(fit_curve=fit_curve, fit_result=self.fit_result, snr=snr) + ds.add_figure(self.name, fig=fig) + + image_path = ds._new_file_path(ds.savefolders[1], self.name, suffix="png") + self.figure_paths.append(image_path) + + def evaluate(self) -> OperationStatus: + """ + Evaluate if the fit was successful based on SNR threshold. + If successful, update the amplitude output parameter with the fitted amplitude value. + """ + header = ( + f"## Gaussian - Amplitude Fit\n" + f"Generated fake Gaussian data and fitted it to extract amplitude.\n" + f"Data Path: `{self.data_loc}`\n" + f"Plot:\n" + ) + plot_image = self.figure_paths[0].resolve() + + assert self.snr is not None + assert self.fit_result is not None + if self.snr >= self.SNR_THRESHOLD: + logger.info( + f"SNR of {self.snr} is bigger than threshold of {self.SNR_THRESHOLD}. Applying new values" + ) + + old_value = self.amplitude() + new_value = self.fit_result.params["A"].value # Gaussian amplitude + + logger.info( + f"Updating {self.amplitude.name} from {old_value} to {new_value}" + ) + self.amplitude(new_value) + + self.improvements = [ParamImprovement(old_value, new_value, self.amplitude)] + + msg_2 = ( + f"Fit was **SUCCESSFUL** with an SNR of {self.snr:.3f}.\n" + f"{self.amplitude.name} updated: {old_value} -> {new_value:.3f}\n\n" + f"**Fit Report:**\n```\n{str(self.fit_result.lmfit_result.fit_report())}\n```\n\n" + ) + + self.report_output.append(header) + self.report_output.append(plot_image) + self.report_output.append(msg_2) + + if self.total_attempts_made != 3: + msg_3 = f"Protocol at {self.total_attempts_made} repetitions, repeating for testing." + self.report_output.append(msg_3) + return OperationStatus.RETRY + + return OperationStatus.SUCCESS + + logger.info( + f"SNR of {self.snr} is smaller than threshold of {self.SNR_THRESHOLD}. Evaluation failed" + ) + + msg_2 = ( + f"Fit was **UNSUCCESSFUL** with an SNR of {self.snr:.3f}.\n" + f"NO value has been changed.\n" + f"Fit Report:\n\n```\n{str(self.fit_result.lmfit_result.fit_report())}\n```\n" + ) + self.report_output = [header, plot_image, msg_2] + + return OperationStatus.FAILURE diff --git a/src/labcore/testing/protocol_dummy/linear.py b/src/labcore/testing/protocol_dummy/linear.py new file mode 100644 index 0000000..2ac6ab5 --- /dev/null +++ b/src/labcore/testing/protocol_dummy/linear.py @@ -0,0 +1,172 @@ +import logging +from pathlib import Path +from typing import Any, cast + +import matplotlib.pyplot as plt +import numpy as np + +from labcore.analysis import DatasetAnalysis +from labcore.analysis.fit import FitResult +from labcore.analysis.fitfuncs.generic import Linear +from labcore.data.datadict_storage import datadict_from_hdf5 +from labcore.measurement.record import dependent, independent, recording +from labcore.measurement.storage import run_and_save_sweep +from labcore.measurement.sweep import Sweep +from labcore.protocols.base import OperationStatus, ParamImprovement, ProtocolOperation +from labcore.testing.protocol_dummy.parameters import LinearOffset, LinearSlope + +plt.switch_backend("agg") + +logger = logging.getLogger(__name__) + + +class LinearOperation(ProtocolOperation): + SNR_THRESHOLD = 2 + + def __init__(self, params: Any = None) -> None: + super().__init__() + + self.offset: LinearOffset + self._register_inputs(offset=LinearOffset(params)) + self.slope: LinearSlope + self._register_outputs(slope=LinearSlope(params)) + + self.condition = f"Success if the SNR of the Linear fit is bigger than the current threshold of {self.SNR_THRESHOLD}" + + self.independents = {"x_values": []} + self.dependents = {"y_values": []} + + self.fit_result: FitResult | None = None + self.snr: float | None = None + + def _measure_dummy(self) -> Path: + """ + Creates fake data that looks like a Linear function with noise using a sweep. + Model: m * x + of + """ + logger.info("Starting Linear measurement (generating fake Linear data)") + + # True Linear parameters + true_slope = 2.5 + true_offset = 3.0 + + # Create x values for the sweep + x_values = np.linspace(-5, 5, 50) + + # Define a measurement function that generates Linear data with noise + @recording(independent("x"), dependent("y")) + def measure_linear(x_val: float) -> tuple[float, float]: + """Generate a single Linear data point with noise""" + y_clean = true_slope * x_val + true_offset + noise = np.random.normal(0, 0.5) + return x_val, y_clean + noise + + # Create the sweep using Sweep directly + sweep = Sweep(x_values, measure_linear) + + # Run and save the sweep + logger.debug("Sweep created, running measurement") + loc, data_array = run_and_save_sweep(sweep, "data", self.name) + logger.info(f"Measurement complete, data saved to {loc}") + + return Path(loc) + + def _load_data_dummy(self) -> None: + """Load the generated fake data""" + assert self.data_loc is not None + path = self.data_loc / "data.ddh5" + if not path.exists(): + raise FileNotFoundError(f"File {path} does not exist") + data = datadict_from_hdf5(path) + + self.independents["x_values"] = data["x"]["values"] + self.dependents["y_values"] = data["y"]["values"] + + def analyze(self) -> None: + """Fit the data to a Linear function""" + assert self.data_loc is not None + with DatasetAnalysis(self.data_loc, self.name) as ds: + x = np.asarray(self.independents["x_values"]) + y = np.asarray(self.dependents["y_values"]) + + # Perform Linear fit + fit = Linear(x, y) + self.fit_result = cast(FitResult, fit.run()) + fit_curve = self.fit_result.eval() + residuals = y - fit_curve + + # Calculate SNR + # Use relative noise to avoid bias from y-value range + signal_range = np.max(np.abs(fit_curve)) - np.min(np.abs(fit_curve)) + noise = np.std(residuals) + # SNR based on noise relative to signal range + snr = float(np.abs(signal_range / (4 * noise))) + self.snr = snr + + # Create plot + fig, ax = plt.subplots() + ax.set_title("Linear - Slope Fit") + ax.set_xlabel("X Values (A.U)") + ax.set_ylabel("Y Values (A.U)") + ax.plot(x, y, "o", label="Data", markersize=4) + ax.plot(x, fit_curve, "-", label="Linear Fit", linewidth=2) + ax.legend() + ax.grid(True, alpha=0.3) + + # Save results + ds.add(fit_curve=fit_curve, fit_result=self.fit_result, snr=snr) + ds.add_figure(self.name, fig=fig) + + image_path = ds._new_file_path(ds.savefolders[1], self.name, suffix="png") + self.figure_paths.append(image_path) + + def evaluate(self) -> OperationStatus: + """ + Evaluate if the fit was successful based on SNR threshold. + If successful, update the slope output parameter with the fitted slope value. + """ + header = ( + f"## Linear - Slope Fit\n" + f"Generated fake Linear data and fitted it to extract slope.\n" + f"Data Path: `{self.data_loc}`\n" + f"Plot:\n" + ) + plot_image = self.figure_paths[0].resolve() + + assert self.snr is not None + assert self.fit_result is not None + if self.snr >= self.SNR_THRESHOLD: + logger.info( + f"SNR of {self.snr} is bigger than threshold of {self.SNR_THRESHOLD}. Applying new values" + ) + + old_value = self.slope() + new_value = self.fit_result.params["m"].value + + logger.info(f"Updating {self.slope.name} from {old_value} to {new_value}") + self.slope(new_value) + + self.improvements = [ParamImprovement(old_value, new_value, self.slope)] + + msg_2 = ( + f"Fit was **SUCCESSFUL** with an SNR of {self.snr:.3f}.\n" + f"{self.slope.name} updated: {old_value} -> {new_value:.3f}\n\n" + f"**Fit Report:**\n```\n{str(self.fit_result.lmfit_result.fit_report())}\n```\n\n" + ) + + self.report_output = [header, plot_image, msg_2] + + return OperationStatus.SUCCESS + + logger.info( + f"SNR of {self.snr} is smaller than threshold of {self.SNR_THRESHOLD}. Evaluation failed" + ) + + msg_2 = ( + f"Fit was **UNSUCCESSFUL** with an SNR of {self.snr:.3f}.\n" + f"NO value has been changed.\n" + f"Fit Report:\n\n```\n{str(self.fit_result.lmfit_result.fit_report())}\n```\n" + ) + self.report_output = [header, plot_image, msg_2] + + return OperationStatus.FAILURE diff --git a/src/labcore/testing/protocol_dummy/parameters.py b/src/labcore/testing/protocol_dummy/parameters.py new file mode 100644 index 0000000..98253b9 --- /dev/null +++ b/src/labcore/testing/protocol_dummy/parameters.py @@ -0,0 +1,207 @@ +from dataclasses import dataclass, field + +from labcore.protocols.base import ProtocolParameterBase + + +@dataclass +class _DummyParameterBase(ProtocolParameterBase): + """Base for all dummy protocol parameters. + + Provides simple in-memory value storage via the DUMMY getter/setter so + that ``param()`` and ``param(value)`` work without any external hardware. + The stored value is initialised to ``0.0`` in ``__post_init__``. + """ + + def __post_init__(self) -> None: + super().__post_init__() + self._value: float = 0.0 + + def _dummy_getter(self) -> float: + return self._value + + def _dummy_setter(self, v: float) -> None: + self._value = v + + +# --------------------------------------------------------------------------- +# Gaussian parameters: A * exp(-((x - x0)^2) / (2 * sigma^2)) +# --------------------------------------------------------------------------- + + +@dataclass +class GaussianCenter(_DummyParameterBase): + name: str = field(default="gaussian_center", init=False) + description: str = field(default="Center position (x0) of the Gaussian", init=False) + qick_path: str = field(default="", init=False) + + +@dataclass +class GaussianSigma(_DummyParameterBase): + name: str = field(default="gaussian_sigma", init=False) + description: str = field(default="Width (sigma) of the Gaussian", init=False) + qick_path: str = field(default="", init=False) + + +@dataclass +class GaussianOffset(_DummyParameterBase): + name: str = field(default="gaussian_offset", init=False) + description: str = field(default="Offset (y0) of the Gaussian baseline", init=False) + qick_path: str = field(default="", init=False) + + +@dataclass +class GaussianAmplitude(_DummyParameterBase): + name: str = field(default="gaussian_amplitude", init=False) + description: str = field(default="Amplitude (A) of the Gaussian peak", init=False) + qick_path: str = field(default="", init=False) + + +# --------------------------------------------------------------------------- +# Cosine parameters: A * cos(2*pi*f*x + phi) + of +# --------------------------------------------------------------------------- + + +@dataclass +class CosineAmplitude(_DummyParameterBase): + name: str = field(default="cosine_amplitude", init=False) + description: str = field(default="Amplitude (A) of the cosine", init=False) + qick_path: str = field(default="", init=False) + + +@dataclass +class CosineFrequency(_DummyParameterBase): + name: str = field(default="cosine_frequency", init=False) + description: str = field(default="Frequency (f) of the cosine", init=False) + qick_path: str = field(default="", init=False) + + +@dataclass +class CosinePhase(_DummyParameterBase): + name: str = field(default="cosine_phase", init=False) + description: str = field(default="Phase (phi) of the cosine", init=False) + qick_path: str = field(default="", init=False) + + +@dataclass +class CosineOffset(_DummyParameterBase): + name: str = field(default="cosine_offset", init=False) + description: str = field(default="Offset (of) of the cosine", init=False) + qick_path: str = field(default="", init=False) + + +# --------------------------------------------------------------------------- +# Exponential parameters: a * b^x +# --------------------------------------------------------------------------- + + +@dataclass +class ExponentialA(_DummyParameterBase): + name: str = field(default="exponential_a", init=False) + description: str = field(default="Coefficient (a) of the exponential", init=False) + qick_path: str = field(default="", init=False) + + +@dataclass +class ExponentialB(_DummyParameterBase): + name: str = field(default="exponential_b", init=False) + description: str = field(default="Base (b) of the exponential", init=False) + qick_path: str = field(default="", init=False) + + +# --------------------------------------------------------------------------- +# ExponentialDecay parameters: A * exp(-x/tau) + of +# --------------------------------------------------------------------------- + + +@dataclass +class ExponentialDecayAmplitude(_DummyParameterBase): + name: str = field(default="exponential_decay_amplitude", init=False) + description: str = field( + default="Amplitude (A) of the exponential decay", init=False + ) + qick_path: str = field(default="", init=False) + + +@dataclass +class ExponentialDecayOffset(_DummyParameterBase): + name: str = field(default="exponential_decay_offset", init=False) + description: str = field(default="Offset (of) of the exponential decay", init=False) + qick_path: str = field(default="", init=False) + + +@dataclass +class ExponentialDecayTau(_DummyParameterBase): + name: str = field(default="exponential_decay_tau", init=False) + description: str = field( + default="Time constant (tau) of the exponential decay", init=False + ) + qick_path: str = field(default="", init=False) + + +# --------------------------------------------------------------------------- +# Linear parameters: m * x + of +# --------------------------------------------------------------------------- + + +@dataclass +class LinearSlope(_DummyParameterBase): + name: str = field(default="linear_slope", init=False) + description: str = field(default="Slope (m) of the linear function", init=False) + qick_path: str = field(default="", init=False) + + +@dataclass +class LinearOffset(_DummyParameterBase): + name: str = field(default="linear_offset", init=False) + description: str = field(default="Offset (of) of the linear function", init=False) + qick_path: str = field(default="", init=False) + + +# --------------------------------------------------------------------------- +# ExponentiallyDecayingSine parameters: A * sin(2*pi*(f*x + phi/360)) * exp(-x/tau) + of +# --------------------------------------------------------------------------- + + +@dataclass +class ExponentiallyDecayingSineAmplitude(_DummyParameterBase): + name: str = field(default="exp_decay_sine_amplitude", init=False) + description: str = field( + default="Amplitude (A) of the exponentially decaying sine", init=False + ) + qick_path: str = field(default="", init=False) + + +@dataclass +class ExponentiallyDecayingSineOffset(_DummyParameterBase): + name: str = field(default="exp_decay_sine_offset", init=False) + description: str = field( + default="Offset (of) of the exponentially decaying sine", init=False + ) + qick_path: str = field(default="", init=False) + + +@dataclass +class ExponentiallyDecayingSineFrequency(_DummyParameterBase): + name: str = field(default="exp_decay_sine_frequency", init=False) + description: str = field( + default="Frequency (f) of the exponentially decaying sine", init=False + ) + qick_path: str = field(default="", init=False) + + +@dataclass +class ExponentiallyDecayingSinePhase(_DummyParameterBase): + name: str = field(default="exp_decay_sine_phase", init=False) + description: str = field( + default="Phase (phi) of the exponentially decaying sine", init=False + ) + qick_path: str = field(default="", init=False) + + +@dataclass +class ExponentiallyDecayingSineTau(_DummyParameterBase): + name: str = field(default="exp_decay_sine_tau", init=False) + description: str = field( + default="Time constant (tau) of the exponentially decaying sine", init=False + ) + qick_path: str = field(default="", init=False) diff --git a/src/labcore/testing/resonator_readout_data.py b/src/labcore/testing/resonator_readout_data.py new file mode 100755 index 0000000..a5bc336 --- /dev/null +++ b/src/labcore/testing/resonator_readout_data.py @@ -0,0 +1,75 @@ +from typing import Tuple + +import numpy as np + +from labcore.data.datadict import DataDict, str2dd + +# Define constants and parameters +amplitude = 2 # Amplitude of the resonator response +noise_level = 0.2 # Noise level + + +# Simulate the resonator response +def simulate_S21( + center_frequency: float, Q_factor: float, frequency_range: float, num_points: int +) -> Tuple[np.ndarray, np.ndarray]: + frequencies = np.linspace( + center_frequency - frequency_range / 2, + center_frequency + frequency_range / 2, + num_points, + ) + response = amplitude / ( + 1 + 1j * (frequencies - center_frequency) / (center_frequency / Q_factor) + ) + response += np.random.normal(0, noise_level, len(frequencies)) + return response, frequencies + + +def resonator_dataset( + center_frequency: float, + Q_factor: float, + frequency_range: float, + reps: int = 10, + num_points: int = 100, +) -> DataDict: + data = str2dd("signal(repetition, fs); fs[Hz]; testing[s];") + response, frequencies = simulate_S21( + center_frequency, Q_factor, frequency_range, num_points + ) + for i in range(reps): + for j in range(100): + data.add_data( + signal=response, + fs=frequencies, + testing=[j], + repetition=np.arange(num_points, dtype=int) + 1, + ) + return data + + +# Plot the resonator response + + +def plot_resonator_response(frequencies: np.ndarray, response: np.ndarray) -> None: + plt.figure() + plt.plot(frequencies, np.abs(response), label="Amplitude") + plt.xlabel("Frequency (Hz)") + plt.ylabel("Amplitude") + plt.legend() + plt.title("Microwave Resonator Response") + plt.grid(True) + plt.show() + + +# Main function +if __name__ == "__main__": + from matplotlib import pyplot as plt + + center_frequency = 5e9 # Center frequency in Hz + frequency_range = 1e9 # Frequency range in Hz + Q_factor = 100 + num_points = 2000 + response, frequencies = simulate_S21( + center_frequency, Q_factor, frequency_range, num_points + ) + plot_resonator_response(frequencies, response) diff --git a/labcore/instruments/__init__.py b/src/labcore/utils/__init__.py similarity index 100% rename from labcore/instruments/__init__.py rename to src/labcore/utils/__init__.py diff --git a/labcore/utils/misc.py b/src/labcore/utils/misc.py similarity index 80% rename from labcore/utils/misc.py rename to src/labcore/utils/misc.py index 42a5976..5b22ea2 100644 --- a/labcore/utils/misc.py +++ b/src/labcore/utils/misc.py @@ -3,17 +3,18 @@ Various utility functions. """ +import inspect import logging from enum import Enum -from pathlib import Path from importlib.metadata import distributions -from typing import List, Tuple, TypeVar, Optional, Sequence, Any, Callable, Union -import inspect +from pathlib import Path +from typing import Any, Callable, List, Optional, Sequence, Tuple, TypeVar, Union -from git import Repo, InvalidGitRepositoryError +from git import InvalidGitRepositoryError, Repo logger = logging.getLogger(__name__) + def reorder_indices(lst: Sequence[str], target: Sequence[str]) -> Tuple[int, ...]: """ Determine how to bring a list with unique entries to a different order. @@ -26,11 +27,11 @@ def reorder_indices(lst: Sequence[str], target: Sequence[str]) -> Tuple[int, ... :raises: ``ValueError`` for invalid inputs. """ if set([type(i) for i in lst]) != {str}: - raise ValueError('Only lists of strings are supported') + raise ValueError("Only lists of strings are supported") if len(set(lst)) < len(lst): - raise ValueError('Input list elements are not unique.') + raise ValueError("Input list elements are not unique.") if set(lst) != set(target) or len(lst) != len(target): - raise ValueError('Contents of input and target do not match.') + raise ValueError("Contents of input and target do not match.") idxs = [] for elt in target: @@ -39,8 +40,7 @@ def reorder_indices(lst: Sequence[str], target: Sequence[str]) -> Tuple[int, ... return tuple(idxs) -def reorder_indices_from_new_positions(lst: List[str], **pos: int) \ - -> Tuple[int, ...]: +def reorder_indices_from_new_positions(lst: List[str], **pos: int) -> Tuple[int, ...]: """ Determine how to bring a list with unique entries to a different order. @@ -51,9 +51,9 @@ def reorder_indices_from_new_positions(lst: List[str], **pos: int) \ :raises: ``ValueError`` for invalid inputs. """ if set([type(i) for i in lst]) != {str}: - raise ValueError('Only lists of strings are supported') + raise ValueError("Only lists of strings are supported") if len(set(lst)) < len(lst): - raise ValueError('Input list elements are not unique.') + raise ValueError("Input list elements are not unique.") target = lst.copy() for item, newidx in pos.items(): @@ -64,7 +64,7 @@ def reorder_indices_from_new_positions(lst: List[str], **pos: int) \ return reorder_indices(lst, target) -T = TypeVar('T') +T = TypeVar("T") def unwrap_optional(val: Optional[T]) -> T: @@ -120,10 +120,12 @@ def fromLabel(cls, label: str) -> Optional["LabeledOptions"]: return k return None - # FIXME: 'None' should never overrides a default! -def map_input_to_signature(func: Union[Callable, inspect.Signature], - *args: Any, **kwargs: Any): + + +def map_input_to_signature( + func: Union[Callable, inspect.Signature], *args: Any, **kwargs: Any +) -> tuple[list[Any], dict[str, Any]]: """Try to re-organize the positional arguments `args` and key word arguments `kwargs` such that `func` can be called with them. @@ -152,9 +154,9 @@ def map_input_to_signature(func: Union[Callable, inspect.Signature], ... myfunc(*args, **kwargs) x=5, y=1, z=2 """ - args = list(args) - func_args = [] - func_kwargs = {} + args_list: list[Any] = list(args) + func_args: list[Any] = [] + func_kwargs: dict[str, Any] = {} if isinstance(func, inspect.Signature): sig = func @@ -170,13 +172,15 @@ def map_input_to_signature(func: Union[Callable, inspect.Signature], # we treat anything that can be given positionally as positional. # first prio to keyword-given values, second to positionally given, # finally default if given in signature. - if p_.kind in [inspect.Parameter.POSITIONAL_OR_KEYWORD, - inspect.Parameter.POSITIONAL_ONLY]: + if p_.kind in [ + inspect.Parameter.POSITIONAL_OR_KEYWORD, + inspect.Parameter.POSITIONAL_ONLY, + ]: if p in kwargs: func_args.insert(idx, kwargs.pop(p)) else: - if len(args) > 0: - func_args.insert(idx, args.pop(0)) + if len(args_list) > 0: + func_args.insert(idx, args_list.pop(0)) elif p_.default is inspect.Parameter.empty: func_args.insert(idx, None) else: @@ -187,7 +191,7 @@ def map_input_to_signature(func: Union[Callable, inspect.Signature], func_kwargs[p] = kwargs.pop(p) elif p_.kind is inspect.Parameter.VAR_POSITIONAL: - for a in args: + for a in args_list: func_args.append(a) elif p_.kind is inspect.Parameter.VAR_KEYWORD: @@ -197,31 +201,35 @@ def map_input_to_signature(func: Union[Callable, inspect.Signature], def indent_text(text: str, level: int = 0) -> str: - """Indent each line of ``text`` by ``level`` spaces.""" - return "\n".join([" " * level + line for line in text.split('\n')]) + """Indent each line of ``text`` by ``level`` spaces.""" + return "\n".join([" " * level + line for line in text.split("\n")]) -def get_environment_packages(): +def get_environment_packages() -> dict[str, str]: """ Generates a dictionary with the names of the installed packages and their current version. It detects if a package was installed in development mode and places the current commit hash instead of the version. """ packages = {} for dist in distributions(): - package_name = dist.metadata['Name'] + package_name = dist.metadata["Name"] version = dist.version - location = Path(dist.locate_file('')) + location = Path(str(dist.locate_file(""))) try: repo = Repo(location, search_parent_directories=True) if repo.is_dirty(): - raise RuntimeError(f"There are uncommitted changes in tracked files in {location}.") + raise RuntimeError( + f"There are uncommitted changes in tracked files in {location}." + ) commit = repo.head.commit.hexsha packages[package_name] = commit except (InvalidGitRepositoryError, RuntimeError) as e: if isinstance(e, RuntimeError): - logger.warning(f"The package {package_name} has uncommitted changes. Will not be tracked. Please fix") - packages[package_name] = 'uncommitted-changes' + logger.warning( + f"The package {package_name} has uncommitted changes. Will not be tracked. Please fix" + ) + packages[package_name] = "uncommitted-changes" elif isinstance(e, InvalidGitRepositoryError): # Editable packages might appear twice in the list of distributions. If the one pointing to the repo appears second, it will get overwritten. if package_name not in packages: @@ -239,7 +247,9 @@ def commit_changes_in_repo(current_dir: Path) -> Optional[str]: repo = Repo(current_dir, search_parent_directories=True) if repo.is_dirty(untracked_files=True): repo.git.add(A=True) - repo.git.commit('-m', '[Auto-commit] Save changes before running measurement') + repo.git.commit( + "-m", "[Auto-commit] Save changes before running measurement" + ) commit_hash = repo.head.commit.hexsha return commit_hash commit_hash = repo.head.commit.hexsha diff --git a/labcore/utils/num.py b/src/labcore/utils/num.py similarity index 85% rename from labcore/utils/num.py rename to src/labcore/utils/num.py index 3d841c4..91c1ce2 100644 --- a/labcore/utils/num.py +++ b/src/labcore/utils/num.py @@ -2,7 +2,8 @@ Tools for numerical operations. """ -from typing import List, Optional, Sequence, Tuple, Union + +from typing import List, Optional, Tuple, Union import numpy as np import pandas as pd @@ -10,13 +11,21 @@ from ..utils.misc import unwrap_optional INTTYPES = [int, np.int16, np.int32, np.int64] -FLOATTYPES = [float, np.float16, np.float32, np.float64, - complex, np.complex64, np.complex128] +FLOATTYPES = [ + float, + np.float16, + np.float32, + np.float64, + complex, + np.complex64, + np.complex128, +] NUMTYPES = INTTYPES + FLOATTYPES -def largest_numtype(arr: np.ndarray, include_integers: bool = True) \ - -> Union[None, type]: +def largest_numtype( + arr: np.ndarray, include_integers: bool = True +) -> Union[None, type]: """ Get the largest numerical type present in an array. :param arr: input array @@ -46,7 +55,9 @@ def largest_numtype(arr: np.ndarray, include_integers: bool = True) \ return None -def _are_close(a: np.ndarray, b: np.ndarray, rtol: float = 1e-8) -> Union[np.ndarray, np.bool_]: +def _are_close( + a: np.ndarray, b: np.ndarray, rtol: float = 1e-8 +) -> Union[np.ndarray, np.bool_]: return np.isclose(a, b, rtol=rtol) @@ -57,7 +68,7 @@ def _are_equal(a: np.ndarray, b: np.ndarray) -> np.ndarray: def is_invalid(a: np.ndarray) -> np.ndarray: # really use == None to do an element wise # check for None - isnone = a == None + isnone = a is None if a.dtype in FLOATTYPES: isnan = np.isnan(a) else: @@ -69,8 +80,7 @@ def _are_invalid(a: np.ndarray, b: np.ndarray) -> np.ndarray: return is_invalid(a) & is_invalid(b) -def arrays_equal(a: np.ndarray, b: np.ndarray, - rtol: Optional[float] = None) -> bool: +def arrays_equal(a: np.ndarray, b: np.ndarray, rtol: Optional[float] = None) -> bool: """Check if two numpy arrays are equal, content-wise. Perform the following checks: @@ -99,9 +109,9 @@ def arrays_equal(a: np.ndarray, b: np.ndarray, return bool(np.all(equal | close | invalid)) -def array1d_to_meshgrid(arr: Union[List, np.ndarray], - target_shape: Tuple[int, ...], - copy: bool = True) -> np.ndarray: +def array1d_to_meshgrid( + arr: Union[List, np.ndarray], target_shape: Tuple[int, ...], copy: bool = True +) -> np.ndarray: """ reshape an array to a target shape. @@ -136,14 +146,12 @@ def array1d_to_meshgrid(arr: Union[List, np.ndarray], return localarr.reshape(target_shape) -def _find_switches(arr: np.ndarray, - rth: float = 25, - ztol: float = 1e-15) -> np.ndarray: +def _find_switches(arr: np.ndarray, rth: float = 25, ztol: float = 1e-15) -> np.ndarray: arr_: np.ndarray = np.ma.MaskedArray(arr, is_invalid(arr)) deltas = arr_[1:] - arr_[:-1] - hi = np.percentile(arr[~is_invalid(arr)], 100.-rth) + hi = np.percentile(arr[~is_invalid(arr)], 100.0 - rth) lo = np.percentile(arr[~is_invalid(arr)], rth) - diff = np.abs(hi-lo) + diff = np.abs(hi - lo) if not diff > ztol: return np.array([]) @@ -157,19 +165,23 @@ def _find_switches(arr: np.ndarray, # importantly: switches have to opposite to the sweep direction. # we check the sweep direction by looking at the values prior to the # first suspected switch - sweep_direction = np.sign(np.mean(deltas[:switch_candidates[0]])) + sweep_direction = np.sign(np.mean(deltas[: switch_candidates[0]])) # real switches are then those where the delta is opposite to the sweep # direction. switch_candidate_vals = deltas[switch_candidates] - switches = [s for (s, v) in zip(switch_candidates, switch_candidate_vals) - if np.sign(v) == -sweep_direction] + switches = [ + s + for (s, v) in zip(switch_candidates, switch_candidate_vals) + if np.sign(v) == -sweep_direction + ] return np.array(switches) -def find_direction_period(vals: np.ndarray, ignore_last: bool = False) \ - -> Optional[float]: +def find_direction_period( + vals: np.ndarray, ignore_last: bool = False +) -> Optional[float]: """ Find the period with which the values in an array change direction. @@ -189,14 +201,14 @@ def find_direction_period(vals: np.ndarray, ignore_last: bool = False) \ # if there was one switch there is a period only if the switch occurred # in the second half of the data elif len(switches) == 1: - if switches[0] >= (vals.size / 2.) - 1: + if switches[0] >= (vals.size / 2.0) - 1: return switches[0] + 1 else: return None if switches[-1] < vals.size - 1: - switches = np.append(switches, vals.size-1) - periods = (switches[1:] - switches[:-1]) + switches = np.append(switches, vals.size - 1) + periods = switches[1:] - switches[:-1] if ignore_last and periods[-1] < periods[0]: periods = periods[:-1] @@ -209,8 +221,9 @@ def find_direction_period(vals: np.ndarray, ignore_last: bool = False) \ return int(periods[0]) -def guess_grid_from_sweep_direction(**axes: np.ndarray) \ - -> Union[None, Tuple[List[str], Tuple[int, ...]]]: +def guess_grid_from_sweep_direction( + **axes: np.ndarray, +) -> Union[None, Tuple[List[str], Tuple[int, ...]]]: """ Try to determine order and shape of a set of axes data (such as flattened meshgrid data). @@ -235,7 +248,8 @@ def guess_grid_from_sweep_direction(**axes: np.ndarray) \ for name, vals in axes.items(): if len(np.array(vals).shape) > 1: raise ValueError( - f"Expect 1-dimensional axis data, not {np.array(vals).shape}") + f"Expect 1-dimensional axis data, not {np.array(vals).shape}" + ) if size is None: size = np.array(vals).size else: @@ -265,7 +279,7 @@ def guess_grid_from_sweep_direction(**axes: np.ndarray) \ else: if mean == 0: mean = max(np.abs(vals.max()), np.abs(vals.min())) - cost = 1./np.abs(np.std(vals)/mean) + cost = 1.0 / np.abs(np.std(vals) / mean) sorting.append(size + cost) else: return None @@ -280,7 +294,6 @@ def guess_grid_from_sweep_direction(**axes: np.ndarray) \ divisor = 1 for i, p in enumerate(periods): - # we need to treat the non-repeating dimensions correctly. # if there's a rest when diving on a dimension that has no defined period, # that means we have to add 1 to the period to get its length. @@ -301,7 +314,7 @@ def guess_grid_from_sweep_direction(**axes: np.ndarray) \ # at this point, `divisor` is the size of the full grid. # it cannot be smaller than the total size, but can be larger # for incomplete grids. - if (divisor < size): + if divisor < size: return None # in returning, we go back to standard order, i.e., slow->fast. @@ -318,7 +331,7 @@ def crop2d_rows_cols(arr: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: :raises: ``ValueError`` if input is not a 2d ndarray. """ if len(arr.shape) != 2: - raise ValueError('input is not a 2d array.') + raise ValueError("input is not a 2d array.") invalids = is_invalid(arr) ys = np.where(np.all(invalids, axis=0))[0] @@ -348,8 +361,7 @@ def joint_crop2d_rows_cols(*arr: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: ) -def crop2d_from_xy(arr: np.ndarray, xs: np.ndarray, - ys: np.ndarray) -> np.ndarray: +def crop2d_from_xy(arr: np.ndarray, xs: np.ndarray, ys: np.ndarray) -> np.ndarray: """ Remove rows/cols from a 2d array. @@ -360,7 +372,7 @@ def crop2d_from_xy(arr: np.ndarray, xs: np.ndarray, :raises: ``ValueError`` if input is not a 2d ndarray. """ if len(arr.shape) != 2: - raise ValueError('input is not a 2d array.') + raise ValueError("input is not a 2d array.") a = arr.copy() a = np.delete(a, xs, axis=0) @@ -368,8 +380,7 @@ def crop2d_from_xy(arr: np.ndarray, xs: np.ndarray, return a -def crop2d(x: np.ndarray, y: np.ndarray, *arr: np.ndarray) \ - -> Tuple[np.ndarray, ...]: +def crop2d(x: np.ndarray, y: np.ndarray, *arr: np.ndarray) -> Tuple[np.ndarray, ...]: """ Remove invalid rows and columns from 2d data. @@ -387,8 +398,7 @@ def crop2d(x: np.ndarray, y: np.ndarray, *arr: np.ndarray) \ return tuple(ret) -def interp_meshgrid_2d(xx: np.ndarray, - yy: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: +def interp_meshgrid_2d(xx: np.ndarray, yy: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: """ Try to find missing vertices in a 2d meshgrid, where xx and yy are the x and y coordinates of each point. @@ -419,7 +429,7 @@ def centers2edges_1d(arr: np.ndarray) -> np.ndarray: They are equidistantly spaced between the centers, such that the centers are in the middle between the vertices. """ - e = (arr[1:] + arr[:-1]) / 2. + e = (arr[1:] + arr[:-1]) / 2.0 e = np.concatenate(([arr[0] - (e[0] - arr[0])], e)) e = np.concatenate((e, [arr[-1] + (arr[-1] - e[-1])])) return e @@ -447,28 +457,29 @@ def centers2edges_2d(centers: np.ndarray) -> np.ndarray: # the central vertices are easy -- follow just from the means of the # neighboring centers - center = (centers[1:, 1:] + centers[:-1, :-1] - + centers[:-1, 1:] + centers[1:, :-1]) / 4. + center = ( + centers[1:, 1:] + centers[:-1, :-1] + centers[:-1, 1:] + centers[1:, :-1] + ) / 4.0 edges[1:-1, 1:-1] = center # for the outer edges we just make the vertices such that the points are # pretty much in the center # first average over neighbor centers to get the 'vertical' right - _left = (centers[0, 1:] + centers[0, :-1]) / 2. + _left = (centers[0, 1:] + centers[0, :-1]) / 2.0 # then extrapolate to the left of the centers left = 2 * _left - center[0, :] edges[0, 1:-1] = left # and same for the other three sides - _right = (centers[-1, 1:] + centers[-1, :-1]) / 2. + _right = (centers[-1, 1:] + centers[-1, :-1]) / 2.0 right = 2 * _right - center[-1, :] edges[-1, 1:-1] = right - _top = (centers[1:, 0] + centers[:-1, 0]) / 2. + _top = (centers[1:, 0] + centers[:-1, 0]) / 2.0 top = 2 * _top - center[:, 0] edges[1:-1, 0] = top - _bottom = (centers[1:, -1] + centers[:-1, -1]) / 2. + _bottom = (centers[1:, -1] + centers[:-1, -1]) / 2.0 bottom = 2 * _bottom - center[:, -1] edges[1:-1, -1] = bottom diff --git a/test/pytest/conftest.py b/test/pytest/conftest.py index e904dbf..10ab094 100644 --- a/test/pytest/conftest.py +++ b/test/pytest/conftest.py @@ -3,14 +3,19 @@ from labcore.measurement.record import record_as from labcore.measurement.sweep import sweep_parameter + @pytest.fixture def short_sweep(): - sweep = sweep_parameter('x', range(10), record_as(lambda x: x*2, 'y')) + sweep = sweep_parameter("x", range(10), record_as(lambda x: x * 2, "y")) return sweep @pytest.fixture() def long_sweep(): - sweep = sweep_parameter('a', range(10000), record_as(lambda a: a*2, 'b'), record_as(lambda a: a**2, 'c')) + sweep = sweep_parameter( + "a", + range(10000), + record_as(lambda a: a * 2, "b"), + record_as(lambda a: a**2, "c"), + ) return sweep - diff --git a/test/pytest/test_analysis_base.py b/test/pytest/test_analysis_base.py new file mode 100644 index 0000000..306c5b8 --- /dev/null +++ b/test/pytest/test_analysis_base.py @@ -0,0 +1,380 @@ +"""Tests for labcore.analysis.analysis_base""" + +import json +from pathlib import Path + +import numpy as np +import pandas as pd +import pytest +import xarray as xr + +from labcore.analysis.analysis_base import AnalysisExistsError, DatasetAnalysis + + +def test_init_savefolder_structure(tmp_path): + """__init__ builds analysis and data savefolders with the correct structure. + + savefolders[0] = analysisfolder / name / datafolder.stem + savefolders[1] = datafolder / name + """ + datafolder = tmp_path / "2026-03-02T141356_abc-myexp" + analysis = DatasetAnalysis( + datafolder, name="myanalysis", analysisfolder=tmp_path / "analysis" + ) + + assert ( + analysis.savefolders[0] + == tmp_path / "analysis" / "myanalysis" / datafolder.stem + ) + assert analysis.savefolders[1] == datafolder / "myanalysis" + + +def test_add_stores_entity_and_raises_on_duplicate(tmp_path): + """add() stores an entity by name; raises ValueError on a duplicate key.""" + datafolder = tmp_path / "2026-03-02T141356_abc-myexp" + datafolder.mkdir() + + with DatasetAnalysis( + datafolder, name="myanalysis", analysisfolder=tmp_path / "analysis" + ) as a: + a.add(my_value=42) + assert a.entities["my_value"] == 42 + + with pytest.raises(ValueError): + a.add(my_value=99) + + +def test_load_analysis_data_roundtrip(tmp_path): + """Dict saved via add() can be reloaded with load_analysis_data().""" + datafolder = tmp_path / "2026-03-02T141356_abc-myexp" + datafolder.mkdir() + analysisfolder = tmp_path / "analysis" + + with DatasetAnalysis( + datafolder, name="myanalysis", analysisfolder=analysisfolder + ) as a: + a.add(result={"freq": 5.0, "amp": 1.2}) + + loaded = a.load_analysis_data("result") + assert loaded == {"freq": 5.0, "amp": 1.2} + + +def test_has_analysis_data(tmp_path): + """has_analysis_data() returns True after saving, False for unknown names.""" + datafolder = tmp_path / "2026-03-02T141356_abc-myexp" + datafolder.mkdir() + analysisfolder = tmp_path / "analysis" + + with DatasetAnalysis( + datafolder, name="myanalysis", analysisfolder=analysisfolder + ) as a: + a.add(result={"x": 1}) + + assert a.has_analysis_data("result") is True + assert a.has_analysis_data("nonexistent") is False + + +def test_raise_on_earlier_analysis(tmp_path): + """raise_on_earlier_analysis causes AnalysisExistsError on re-entry when all listed files exist.""" + datafolder = tmp_path / "2026-03-02T141356_abc-myexp" + datafolder.mkdir() + analysisfolder = tmp_path / "analysis" + + # First run — saves a result file + with DatasetAnalysis( + datafolder, name="myanalysis", analysisfolder=analysisfolder + ) as a: + a.add(result={"x": 1}) + + # Second run — should raise because 'result' json already exists + with pytest.raises(AnalysisExistsError): + with DatasetAnalysis( + datafolder, + name="myanalysis", + analysisfolder=analysisfolder, + raise_on_earlier_analysis=[("result", ["json"])], + ) as a: + pass + + +def test_context_manager_saves_dict_to_disk(tmp_path): + """On __exit__, save() writes dict entities as JSON files to both savefolders.""" + datafolder = tmp_path / "2026-03-02T141356_abc-myexp" + datafolder.mkdir() + analysisfolder = tmp_path / "analysis" + + with DatasetAnalysis( + datafolder, name="myanalysis", analysisfolder=analysisfolder + ) as a: + a.add(result={"freq": 5.0, "amp": 1.2}) + + # Both savefolders should contain a JSON file for 'result' + for folder in a.savefolders: + json_files = list(folder.glob("*result*.json")) + assert len(json_files) == 1 + + +def test_save_numpy_scalar(tmp_path): + """Numpy scalar and float entities are saved as JSON files.""" + datafolder = tmp_path / "2026-03-02T141356_abc-myexp" + datafolder.mkdir() + + with DatasetAnalysis( + datafolder, name="myanalysis", analysisfolder=tmp_path / "analysis" + ) as a: + a.add(amplitude=np.float64(3.14)) + + for folder in a.savefolders: + assert len(list(folder.glob("*amplitude*.json"))) == 1 + + +def test_save_string(tmp_path): + """String entities are saved as .txt files.""" + datafolder = tmp_path / "2026-03-02T141356_abc-myexp" + datafolder.mkdir() + + with DatasetAnalysis( + datafolder, name="myanalysis", analysisfolder=tmp_path / "analysis" + ) as a: + a.add(note="this is a note") + + for folder in a.savefolders: + assert len(list(folder.glob("*note*.txt"))) == 1 + + +def test_save_xr_dataset(tmp_path): + """xr.Dataset entities are saved as .nc (netCDF) files.""" + datafolder = tmp_path / "2026-03-02T141356_abc-myexp" + datafolder.mkdir() + + ds = xr.Dataset({"y": ("x", [1.0, 2.0, 3.0])}, coords={"x": [0, 1, 2]}) + with DatasetAnalysis( + datafolder, name="myanalysis", analysisfolder=tmp_path / "analysis" + ) as a: + a.add(data=ds) + + for folder in a.savefolders: + assert len(list(folder.glob("*data*xrdataset*.nc"))) == 1 + + +def test_save_dataframe(tmp_path): + """pd.DataFrame entities are saved as .csv files.""" + datafolder = tmp_path / "2026-03-02T141356_abc-myexp" + datafolder.mkdir() + + df = pd.DataFrame({"x": [1, 2, 3], "y": [4, 5, 6]}) + with DatasetAnalysis( + datafolder, name="myanalysis", analysisfolder=tmp_path / "analysis" + ) as a: + a.add(table=df) + + for folder in a.savefolders: + assert len(list(folder.glob("*table*.csv"))) == 1 + + +def test_save_xr_dataarray(tmp_path): + """xr.DataArray entities are saved as .nc files.""" + datafolder = tmp_path / "2026-03-02T141356_abc-myexp" + datafolder.mkdir() + + da = xr.DataArray([1.0, 2.0, 3.0], dims=["x"], coords={"x": [0, 1, 2]}) + with DatasetAnalysis( + datafolder, name="myanalysis", analysisfolder=tmp_path / "analysis" + ) as a: + a.add(trace=da) + + for folder in a.savefolders: + assert len(list(folder.glob("*trace*xrdataarray*.nc"))) == 1 + + +def test_init_string_datafolder(tmp_path): + """datafolder passed as a string is converted to Path.""" + datafolder = str(tmp_path / "2026-03-02T141356_abc-myexp") + a = DatasetAnalysis( + datafolder, name="myanalysis", analysisfolder=tmp_path / "analysis" + ) + assert isinstance(a.datafolder, Path) + + +def test_init_has_period_in_name(tmp_path): + """has_period_in_name=True includes the suffix in savefolder[0].""" + datafolder = tmp_path / "myexp.h5" + a = DatasetAnalysis( + datafolder, + name="myanalysis", + analysisfolder=tmp_path / "analysis", + has_period_in_name=True, + ) + assert a.savefolders[0] == tmp_path / "analysis" / "myanalysis" / "myexp.h5" + + +def test_save_pickle_fallback(tmp_path): + """Unsupported entity types are saved as pickle files.""" + datafolder = tmp_path / "2026-03-02T141356_abc-myexp" + datafolder.mkdir() + + class CustomObj: + value = 42 + + with DatasetAnalysis( + datafolder, name="myanalysis", analysisfolder=tmp_path / "analysis" + ) as a: + a.add(obj=CustomObj()) + + for folder in a.savefolders: + assert len(list(folder.glob("*obj*.pickle"))) == 1 + + +def test_add_figure(tmp_path): + """add_figure() creates and stores a matplotlib Figure; raises on duplicate.""" + import matplotlib + + matplotlib.use("Agg") + from matplotlib import pyplot as plt + + datafolder = tmp_path / "2026-03-02T141356_abc-myexp" + datafolder.mkdir() + + a = DatasetAnalysis( + datafolder, name="myanalysis", analysisfolder=tmp_path / "analysis" + ) + fig = a.add_figure("myfig") + assert "myfig" in a.entities + assert fig is a.entities["myfig"] + + with pytest.raises(ValueError): + a.add_figure("myfig") + + plt.close("all") + + +def test_save_mpl_figure(tmp_path): + """matplotlib Figure entities are saved as both .png and .pdf.""" + import matplotlib + + matplotlib.use("Agg") + + datafolder = tmp_path / "2026-03-02T141356_abc-myexp" + datafolder.mkdir() + + with DatasetAnalysis( + datafolder, name="myanalysis", analysisfolder=tmp_path / "analysis" + ) as a: + a.add_figure("plot") + + for folder in a.savefolders: + assert len(list(folder.glob("*plot*.png"))) == 1 + assert len(list(folder.glob("*plot*.pdf"))) == 1 + + +def test_save_hv_plot(tmp_path): + """holoviews Dimensioned entities are saved as .html files.""" + import holoviews as hv + import numpy as np + + hv.extension("bokeh") + + datafolder = tmp_path / "2026-03-02T141356_abc-myexp" + datafolder.mkdir() + + curve = hv.Curve(np.linspace(0, 1, 10)) + with DatasetAnalysis( + datafolder, name="myanalysis", analysisfolder=tmp_path / "analysis" + ) as a: + a.add(curve=curve) + + for folder in a.savefolders: + assert len(list(folder.glob("*curve*hvplot*.html"))) == 1 + + +def test_to_table_creates_and_appends(tmp_path): + """to_table() creates a CSV on first call and appends a row on second call.""" + datafolder = tmp_path / "2026-03-02T141356_abc-myexp" + analysisfolder = tmp_path / "analysis" + + a = DatasetAnalysis(datafolder, name="myanalysis", analysisfolder=analysisfolder) + a.to_table("results", {"freq": 5.0}) + + csv_path = a.savefolders[0].parent / "results.csv" + assert csv_path.exists() + df = pd.read_csv(csv_path, index_col=0) + assert len(df) == 1 + assert df["freq"].iloc[0] == 5.0 + + # Second call with different datafolder appends a new row + datafolder2 = tmp_path / "2026-03-03T090000_abc-myexp2" + a2 = DatasetAnalysis(datafolder2, name="myanalysis", analysisfolder=analysisfolder) + a2.to_table("results", {"freq": 6.0}) + + df2 = pd.read_csv(csv_path, index_col=0) + assert len(df2) == 2 + + +def test_analysis_name_groups_multiple_datasets(tmp_path): + """Multiple datasets analyzed with the same name are grouped under analysisfolder/name/. + + analysisfolder/ + T1/ + 2026-03-02T141356_abc-run1/ <- run1 results + 2026-03-02T141356_abc-run2/ <- run2 results + power_rabi/ + 2026-03-02T141356_abc-run3/ <- run3 results + 2026-03-02T141356_abc-run4/ <- run4 results + """ + analysisfolder = tmp_path / "analysis" + + runs = [ + ("2026-03-02T141356_abc-run1", "T1"), + ("2026-03-02T141357_abc-run2", "T1"), + ("2026-03-02T141358_abc-run3", "power_rabi"), + ("2026-03-02T141359_abc-run4", "power_rabi"), + ] + + for folder_name, analysis_name in runs: + datafolder = tmp_path / folder_name + datafolder.mkdir() + with DatasetAnalysis( + datafolder, name=analysis_name, analysisfolder=analysisfolder + ) as a: + a.add(result={"value": 1.0}) + + # Each analysis name produces its own subdirectory + assert (analysisfolder / "T1").is_dir() + assert (analysisfolder / "power_rabi").is_dir() + + # Each dataset gets its own subfolder inside the group + t1_subfolders = list((analysisfolder / "T1").iterdir()) + rabi_subfolders = list((analysisfolder / "power_rabi").iterdir()) + assert len(t1_subfolders) == 2 + assert len(rabi_subfolders) == 2 + + # Subfolder names match the datafolder stems + t1_names = {f.name for f in t1_subfolders} + assert t1_names == {"2026-03-02T141356_abc-run1", "2026-03-02T141357_abc-run2"} + + +def test_load_metadata_from_json(tmp_path): + """load_metadata_from_json() reads a key from a JSON file in the datafolder.""" + datafolder = tmp_path / "2026-03-02T141356_abc-myexp" + datafolder.mkdir() + (datafolder / "meta.json").write_text(json.dumps({"qubit_freq": 5.1, "other": 99})) + + a = DatasetAnalysis(datafolder, name="myanalysis") + assert a.load_metadata_from_json("meta.json", "qubit_freq") == 5.1 + + with pytest.raises(ValueError): + a.load_metadata_from_json("meta.json", "missing_key") + + +def test_load_saved_parameter(tmp_path): + """load_saved_parameter() reads value from a QCoDeS-style parameters.json.""" + datafolder = tmp_path / "2026-03-02T141356_abc-myexp" + datafolder.mkdir() + params = {"parameter_manager.drive_freq": {"value": 4.8, "unit": "GHz"}} + (datafolder / "parameters.json").write_text(json.dumps(params)) + + a = DatasetAnalysis(datafolder, name="myanalysis") + assert a.load_saved_parameter("drive_freq") == 4.8 + + with pytest.raises(ValueError): + a.load_saved_parameter("nonexistent") diff --git a/test/pytest/test_datadict.py b/test/pytest/test_datadict.py new file mode 100644 index 0000000..84d0345 --- /dev/null +++ b/test/pytest/test_datadict.py @@ -0,0 +1,430 @@ +"""Tests for labcore.data.datadict""" + +import numpy as np +import pytest + +from labcore.data.datadict import ( + DataDict, + combine_datadicts, + datadict_to_meshgrid, + datasets_are_equal, + dd2df, + dd2xr, +) + + +def test_datadict_creation_and_structure(): + """DataDict correctly identifies axes vs dependents after validate().""" + dd = DataDict( + x=dict(unit="m"), + z=dict(axes=["x"], unit="V"), + ) + dd.validate() + + assert set(dd.axes()) == {"x"} + assert set(dd.dependents()) == {"z"} + # validate() fills in missing label + assert dd["x"].get("label") == "" + assert dd["z"].get("label") == "" + + +def test_add_data_individual_records(): + """add_data() called once per record appends rows and nrecords() tracks the count.""" + dd = DataDict( + x=dict(unit="m"), + z=dict(axes=["x"]), + ) + dd.add_data(x=1.0, z=10.0) + dd.add_data(x=2.0, z=20.0) + dd.add_data(x=3.0, z=30.0) + + assert dd.nrecords() == 3 + np.testing.assert_array_equal(dd.data_vals("x"), [1.0, 2.0, 3.0]) + np.testing.assert_array_equal(dd.data_vals("z"), [10.0, 20.0, 30.0]) + + +def test_add_data_1d_arrays(): + """add_data() accepts 1D arrays, adding all records at once.""" + dd = DataDict( + x=dict(unit="m"), + z=dict(axes=["x"]), + ) + dd.add_data(x=np.array([1.0, 2.0, 3.0]), z=np.array([10.0, 20.0, 30.0])) + + assert dd.nrecords() == 3 + np.testing.assert_array_equal(dd.data_vals("x"), [1.0, 2.0, 3.0]) + np.testing.assert_array_equal(dd.data_vals("z"), [10.0, 20.0, 30.0]) + + +def test_add_data_multidimensional_arrays(): + """add_data() accepts arrays where each record is itself an array (nested shape).""" + dd = DataDict( + x=dict(unit="m"), + z=dict(axes=["x"]), + ) + # 3 records, each z value is a length-2 array + dd.add_data( + x=np.array([1.0, 2.0, 3.0]), + z=np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]), + ) + + assert dd.nrecords() == 3 + assert dd.data_vals("z").shape == (3, 2) + np.testing.assert_array_equal(dd.data_vals("z")[1], [3.0, 4.0]) + + +def test_dd2df_axes_as_multiindex(): + """dd2df() returns a DataFrame with axes as MultiIndex and dependents as columns.""" + dd = DataDict( + x=dict(unit="m"), + y=dict(unit="s"), + z=dict(axes=["x", "y"]), + ) + dd.add_data( + x=np.array([1.0, 2.0, 3.0]), + y=np.array([4.0, 5.0, 6.0]), + z=np.array([7.0, 8.0, 9.0]), + ) + + df = dd2df(dd) + + assert "z" in df.columns + assert df.index.names == ["x", "y"] + np.testing.assert_array_equal(df["z"].values, [7.0, 8.0, 9.0]) + + +def test_datadict_to_meshgrid_reshapes_flat_sweep(): + """datadict_to_meshgrid() infers grid shape from flat sweep data and reshapes values.""" + # flat sweep: x is slow axis (2 values), y is fast axis (3 values) — 6 rows total + x_vals = np.array([0.0, 0.0, 0.0, 1.0, 1.0, 1.0]) + y_vals = np.array([0.0, 1.0, 2.0, 0.0, 1.0, 2.0]) + z_vals = x_vals * 10 + y_vals # [0, 1, 2, 10, 11, 12] + + dd = DataDict( + x=dict(unit="m", values=x_vals), + y=dict(unit="s", values=y_vals), + z=dict(axes=["x", "y"], values=z_vals), + ) + mgdd = datadict_to_meshgrid(dd) + + assert mgdd.shape() == (2, 3) + np.testing.assert_array_equal(mgdd.data_vals("z"), [[0, 1, 2], [10, 11, 12]]) + + +def test_dd2xr_produces_xarray_dataset(): + """dd2xr() converts a MeshgridDataDict to an xarray Dataset with correct coords and values.""" + # start from an already-gridded MeshgridDataDict + x_vals = np.array([0.0, 0.0, 0.0, 1.0, 1.0, 1.0]) + y_vals = np.array([0.0, 1.0, 2.0, 0.0, 1.0, 2.0]) + z_vals = x_vals * 10 + y_vals + + dd = DataDict( + x=dict(unit="m", values=x_vals), + y=dict(unit="s", values=y_vals), + z=dict(axes=["x", "y"], values=z_vals), + ) + mgdd = datadict_to_meshgrid(dd) + ds = dd2xr(mgdd) + + assert "z" in ds + assert "x" in ds.coords + assert "y" in ds.coords + assert ds["z"].shape == (2, 3) + np.testing.assert_array_equal(ds.coords["x"].values, [0.0, 1.0]) + np.testing.assert_array_equal(ds.coords["y"].values, [0.0, 1.0, 2.0]) + np.testing.assert_array_equal(ds["z"].values, [[0, 1, 2], [10, 11, 12]]) + + +def test_combine_datadicts_merges_different_dependents(): + """combine_datadicts() merges DataDicts with different dependents sharing a common axis.""" + dd1 = DataDict(x=dict(unit="m"), z1=dict(axes=["x"])) + dd1.add_data(x=np.array([1.0, 2.0]), z1=np.array([10.0, 20.0])) + + dd2 = DataDict(x=dict(unit="m"), z2=dict(axes=["x"])) + dd2.add_data(x=np.array([1.0, 2.0]), z2=np.array([100.0, 200.0])) + + combined = combine_datadicts(dd1, dd2) + + # both dependents present under a single shared x axis + assert set(combined.dependents()) == {"z1", "z2"} + assert set(combined.axes()) == {"x"} + np.testing.assert_array_equal(combined.data_vals("z1"), [10.0, 20.0]) + np.testing.assert_array_equal(combined.data_vals("z2"), [100.0, 200.0]) + + +def test_combine_datadicts_different_shapes_and_names(): + """combine_datadicts() with different record counts and variable names downgrades to DataDictBase.""" + dd1 = DataDict(x=dict(unit="m"), z=dict(axes=["x"])) + dd1.add_data(x=np.array([1.0, 2.0]), z=np.array([10.0, 20.0])) + + dd2 = DataDict(t=dict(unit="s"), v=dict(axes=["t"])) + dd2.add_data(t=np.array([0.1, 0.2, 0.3]), v=np.array([5.0, 6.0, 7.0])) + + from labcore.data.datadict import DataDictBase + + combined = combine_datadicts(dd1, dd2) + + assert isinstance(combined, DataDictBase) + assert set(combined.dependents()) == {"z", "v"} + assert set(combined.axes()) == {"x", "t"} + np.testing.assert_array_equal(combined.data_vals("x"), [1.0, 2.0]) + np.testing.assert_array_equal(combined.data_vals("z"), [10.0, 20.0]) + np.testing.assert_array_equal(combined.data_vals("t"), [0.1, 0.2, 0.3]) + np.testing.assert_array_equal(combined.data_vals("v"), [5.0, 6.0, 7.0]) + + +def test_datasets_are_equal(): + """datasets_are_equal() returns True for identical DataDicts and False when values or structure differ.""" + dd1 = DataDict(x=dict(unit="m"), z=dict(axes=["x"])) + dd1.add_data(x=np.array([1.0, 2.0]), z=np.array([10.0, 20.0])) + + # identical copy + dd2 = DataDict(x=dict(unit="m"), z=dict(axes=["x"])) + dd2.add_data(x=np.array([1.0, 2.0]), z=np.array([10.0, 20.0])) + + assert datasets_are_equal(dd1, dd2) + + # different values + dd3 = DataDict(x=dict(unit="m"), z=dict(axes=["x"])) + dd3.add_data(x=np.array([1.0, 2.0]), z=np.array([10.0, 99.0])) + + assert not datasets_are_equal(dd1, dd3) + + # different structure (extra field) + dd4 = DataDict(x=dict(unit="m"), z=dict(axes=["x"]), w=dict(axes=["x"])) + dd4.add_data( + x=np.array([1.0, 2.0]), z=np.array([10.0, 20.0]), w=np.array([0.0, 0.0]) + ) + + assert not datasets_are_equal(dd1, dd4) + + +def test_metadata_global_and_per_field(): + """add_meta() stores global and per-field metadata; meta_val() retrieves it.""" + dd = DataDict(x=dict(unit="m"), z=dict(axes=["x"])) + dd.add_data(x=np.array([1.0, 2.0]), z=np.array([10.0, 20.0])) + + dd.add_meta("sample", "qubit_A") + dd.add_meta("calibrated", True, data="z") + + assert dd.meta_val("sample") == "qubit_A" + assert dd.meta_val("calibrated", data="z") is True + + global_keys = [k for k, _ in dd.meta_items()] + assert "sample" in global_keys + + field_keys = [k for k, _ in dd.meta_items(data="z")] + assert "calibrated" in field_keys + + +def test_eq_identical_datadicts(): + """__eq__ returns True for DataDicts with identical structure and values.""" + dd1 = DataDict(x=dict(unit="m"), z=dict(axes=["x"])) + dd1.add_data(x=np.array([1.0, 2.0]), z=np.array([10.0, 20.0])) + + dd2 = DataDict(x=dict(unit="m"), z=dict(axes=["x"])) + dd2.add_data(x=np.array([1.0, 2.0]), z=np.array([10.0, 20.0])) + + assert dd1 == dd2 + + +def test_eq_different_values(): + """__eq__ returns False when values differ, and False when compared to a non-DataDict.""" + dd1 = DataDict(x=dict(unit="m"), z=dict(axes=["x"])) + dd1.add_data(x=np.array([1.0, 2.0]), z=np.array([10.0, 20.0])) + + dd2 = DataDict(x=dict(unit="m"), z=dict(axes=["x"])) + dd2.add_data(x=np.array([1.0, 2.0]), z=np.array([99.0, 20.0])) + + assert not (dd1 == dd2) + assert not (dd1 == "not a datadict") + + +def test_repr_contains_field_names_and_shape(): + """__repr__ includes dependent and axis names along with their shapes.""" + dd = DataDict(x=dict(unit="m"), z=dict(axes=["x"])) + dd.add_data(x=np.array([1.0, 2.0]), z=np.array([10.0, 20.0])) + + r = repr(dd) + assert "z" in r + assert "x" in r + assert "(2,)" in r + + +def test_datadict_add_concatenates_records(): + """DataDict + DataDict returns a new DataDict with concatenated records.""" + dd1 = DataDict(x=dict(unit="m"), z=dict(axes=["x"])) + dd1.add_data(x=np.array([1.0, 2.0]), z=np.array([10.0, 20.0])) + + dd2 = DataDict(x=dict(unit="m"), z=dict(axes=["x"])) + dd2.add_data(x=np.array([3.0, 4.0]), z=np.array([30.0, 40.0])) + + combined = dd1 + dd2 + + assert combined.nrecords() == 4 + np.testing.assert_array_equal(combined.data_vals("x"), [1.0, 2.0, 3.0, 4.0]) + np.testing.assert_array_equal(combined.data_vals("z"), [10.0, 20.0, 30.0, 40.0]) + + +def test_datadict_append_concatenates_in_place(): + """DataDict.append() extends the DataDict in-place with records from another.""" + dd1 = DataDict(x=dict(unit="m"), z=dict(axes=["x"])) + dd1.add_data(x=np.array([1.0, 2.0]), z=np.array([10.0, 20.0])) + + dd2 = DataDict(x=dict(unit="m"), z=dict(axes=["x"])) + dd2.add_data(x=np.array([3.0, 4.0]), z=np.array([30.0, 40.0])) + + dd1.append(dd2) + + assert dd1.nrecords() == 4 + np.testing.assert_array_equal(dd1.data_vals("x"), [1.0, 2.0, 3.0, 4.0]) + np.testing.assert_array_equal(dd1.data_vals("z"), [10.0, 20.0, 30.0, 40.0]) + + +def test_sanitize_removes_all_nan_rows(): + """sanitize() removes rows where all dependents are NaN, keeps partial rows.""" + dd = DataDict(x=dict(unit="m"), z=dict(axes=["x"])) + dd.add_data( + x=np.array([1.0, 2.0, 3.0, 4.0]), + z=np.array([10.0, np.nan, 30.0, np.nan]), + ) + clean = dd.sanitize() + + # rows where z is NaN (rows 1 and 3) should be removed + assert clean.nrecords() == 2 + np.testing.assert_array_equal(clean.data_vals("x"), [1.0, 3.0]) + np.testing.assert_array_equal(clean.data_vals("z"), [10.0, 30.0]) + + +def test_sanitize_keeps_rows_with_at_least_one_valid_dependent(): + """sanitize() only removes a row if ALL dependents are NaN in that row.""" + dd = DataDict(x=dict(unit="m"), z1=dict(axes=["x"]), z2=dict(axes=["x"])) + dd.add_data( + x=np.array([1.0, 2.0, 3.0]), + z1=np.array([10.0, np.nan, 30.0]), + z2=np.array([100.0, 200.0, np.nan]), + ) + clean = dd.sanitize() + + # no row has ALL dependents NaN, so nothing is removed + assert clean.nrecords() == 3 + + +def test_extract_pulls_dependent_with_its_axes(): + """extract() returns a new DataDict with only the requested dependent and its axes.""" + dd = DataDict( + x=dict(unit="m"), y=dict(unit="s"), z1=dict(axes=["x"]), z2=dict(axes=["y"]) + ) + dd.add_data( + x=np.array([1.0, 2.0]), + y=np.array([3.0, 4.0]), + z1=np.array([10.0, 20.0]), + z2=np.array([30.0, 40.0]), + ) + + extracted = dd.extract(["z1"]) + + assert set(extracted.dependents()) == {"z1"} + assert set(extracted.axes()) == {"x"} + assert "z2" not in extracted + assert "y" not in extracted + np.testing.assert_array_equal(extracted.data_vals("z1"), [10.0, 20.0]) + + +def test_meshgrid_validate_mismatched_shapes_raises(): + """MeshgridDataDict.validate() raises ValueError when dependent shapes don't match.""" + from labcore.data.datadict import MeshgridDataDict + + dd = MeshgridDataDict( + x=dict(values=np.array([[0.0, 1.0], [0.0, 1.0]])), + y=dict(values=np.array([[0.0, 0.0], [1.0, 1.0]])), + z1=dict(axes=["x", "y"], values=np.array([[1.0, 2.0], [3.0, 4.0]])), + z2=dict(axes=["x", "y"], values=np.array([[1.0, 2.0, 3.0]])), # wrong shape + ) + with pytest.raises(ValueError): + dd.validate() + + +def test_meshgrid_validate_non_monotonic_axis_raises(): + """MeshgridDataDict.validate() raises ValueError when an axis is not monotonic.""" + from labcore.data.datadict import MeshgridDataDict + + # x is not monotonic along axis 0 (goes 0, 0, 1, 1 but then back) + dd = MeshgridDataDict( + x=dict(values=np.array([[0.0, 0.0], [0.0, 0.0]])), # no variation along axis 0 + y=dict(values=np.array([[0.0, 1.0], [0.0, 1.0]])), + z=dict(axes=["x", "y"], values=np.array([[1.0, 2.0], [3.0, 4.0]])), + ) + with pytest.raises(ValueError): + dd.validate() + + +def test_expand_flattens_nested_records(): + """expand() flattens nested (per-record) arrays into a 1D sequence.""" + dd = DataDict( + x=dict(unit="m"), + z=dict(axes=["x"]), + ) + # 3 records, each z value is a length-4 array + dd.add_data( + x=np.array([1.0, 2.0, 3.0]), + z=np.array( + [[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0], [9.0, 10.0, 11.0, 12.0]] + ), + ) + + assert not dd.is_expanded() + assert dd.is_expandable() + + expanded = dd.expand() + + assert expanded.is_expanded() + assert expanded.nrecords() == 12 + np.testing.assert_array_equal( + expanded.data_vals("z"), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] + ) + # x is repeated 4 times per original record + np.testing.assert_array_equal( + expanded.data_vals("x"), [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3] + ) + + +def test_datasets_are_equal_different_types(): + """datasets_are_equal() returns False when comparing different DataDict types.""" + + x_vals = np.array([0.0, 0.0, 1.0, 1.0]) + y_vals = np.array([0.0, 1.0, 0.0, 1.0]) + z_vals = np.array([1.0, 2.0, 3.0, 4.0]) + + dd = DataDict( + x=dict(values=x_vals), + y=dict(values=y_vals), + z=dict(axes=["x", "y"], values=z_vals), + ) + mgdd = datadict_to_meshgrid(dd) + + assert not datasets_are_equal(dd, mgdd) + + +def test_datasets_are_equal_ignores_meta(): + """datasets_are_equal() with ignore_meta=True returns True even when metadata differs.""" + dd1 = DataDict(x=dict(unit="m"), z=dict(axes=["x"])) + dd1.add_data(x=np.array([1.0, 2.0]), z=np.array([10.0, 20.0])) + dd1.add_meta("sample", "qubit_A") + + dd2 = DataDict(x=dict(unit="m"), z=dict(axes=["x"])) + dd2.add_data(x=np.array([1.0, 2.0]), z=np.array([10.0, 20.0])) + dd2.add_meta("sample", "qubit_B") + + assert not datasets_are_equal(dd1, dd2) + assert datasets_are_equal(dd1, dd2, ignore_meta=True) + + +def test_validate_mismatched_lengths_raises(): + """validate() raises ValueError when fields have different numbers of records.""" + dd = DataDict( + x=dict(unit="m", values=np.array([1.0, 2.0, 3.0])), + z=dict(axes=["x"], values=np.array([10.0, 20.0])), + ) + with pytest.raises(ValueError): + dd.validate() diff --git a/test/pytest/test_datadict_storage.py b/test/pytest/test_datadict_storage.py new file mode 100644 index 0000000..5c93189 --- /dev/null +++ b/test/pytest/test_datadict_storage.py @@ -0,0 +1,368 @@ +"""Tests for labcore.data.datadict_storage""" + +import numpy as np +import pytest + +from labcore.data.datadict import DataDict, datasets_are_equal +from labcore.data.datadict_storage import ( + AppendMode, + DDH5Writer, + all_datadicts_from_hdf5, + datadict_from_hdf5, + datadict_to_hdf5, + deh5ify, + find_data, + h5ify, + load_as_df, + load_as_xr, + most_recent_data_path, +) + + +@pytest.fixture +def simple_dd(): + dd = DataDict(x=dict(unit="m"), z=dict(axes=["x"])) + dd.add_data(x=np.array([1.0, 2.0, 3.0]), z=np.array([10.0, 20.0, 30.0])) + return dd + + +def test_deh5ify_bytes_to_str(): + """deh5ify decodes a raw bytes object to a plain Python string.""" + assert deh5ify(b"hello") == "hello" + + +def test_h5ify_list_of_strings_passes_through(): + """h5ify leaves a list-of-strings unchanged (all_string=True path).""" + result = h5ify(["a", "b", "c"]) + assert result == ["a", "b", "c"] + + +def test_h5ify_non_string_list_converted_to_array(): + """h5ify converts a list containing non-strings to a numpy array.""" + result = h5ify([1, 2, 3]) + assert isinstance(result, np.ndarray) + np.testing.assert_array_equal(result, [1, 2, 3]) + + +def test_h5ify_deh5ify_string_roundtrip(): + """h5ify encodes a numpy Unicode array to bytes; deh5ify decodes it back.""" + original = np.array(["qubit_A", "qubit_B", "qubit_C"]) # dtype kind 'U' + encoded = h5ify(original) + decoded = deh5ify(encoded) + + assert encoded.dtype.kind == "S" # byte strings + np.testing.assert_array_equal(decoded, original) + + +def test_append_mode_all_duplicates_rows(tmp_path, simple_dd): + """AppendMode.all appends all rows on every write, including duplicates.""" + path = tmp_path / "data.ddh5" + + datadict_to_hdf5(simple_dd, path) + datadict_to_hdf5(simple_dd, path, append_mode=AppendMode.all) + + dd_loaded = datadict_from_hdf5(path) + assert dd_loaded.nrecords() == 6 + np.testing.assert_array_equal( + dd_loaded.data_vals("x"), [1.0, 2.0, 3.0, 1.0, 2.0, 3.0] + ) + + +def test_ddh5writer_creates_file_and_appends(tmp_path): + """DDH5Writer creates data.ddh5 on enter and add_data() appends rows.""" + dd = DataDict(x=dict(unit="m"), z=dict(axes=["x"])) + + with DDH5Writer(dd, basedir=tmp_path, name="test") as writer: + writer.add_data(x=1.0, z=10.0) + writer.add_data(x=2.0, z=20.0) + writer.add_data(x=3.0, z=30.0) + filepath = writer.filepath + + assert filepath.exists() + assert filepath.name == "data.ddh5" + + dd_loaded = datadict_from_hdf5(filepath) + assert dd_loaded.nrecords() == 3 + np.testing.assert_array_equal(dd_loaded.data_vals("x"), [1.0, 2.0, 3.0]) + np.testing.assert_array_equal(dd_loaded.data_vals("z"), [10.0, 20.0, 30.0]) + + +def test_ddh5writer_data_persisted_incrementally(tmp_path): + """Each add_data() call is immediately readable from disk with correct values.""" + dd = DataDict(x=dict(unit="m"), z=dict(axes=["x"])) + + with DDH5Writer(dd, basedir=tmp_path, name="test") as writer: + writer.add_data(x=1.0, z=10.0) + snap1 = datadict_from_hdf5(writer.filepath) + assert snap1.nrecords() == 1 + np.testing.assert_array_equal(snap1.data_vals("x"), [1.0]) + np.testing.assert_array_equal(snap1.data_vals("z"), [10.0]) + + writer.add_data(x=2.0, z=20.0) + snap2 = datadict_from_hdf5(writer.filepath) + assert snap2.nrecords() == 2 + np.testing.assert_array_equal(snap2.data_vals("x"), [1.0, 2.0]) + np.testing.assert_array_equal(snap2.data_vals("z"), [10.0, 20.0]) + + writer.add_data(x=3.0, z=30.0) + + snap3 = datadict_from_hdf5(writer.filepath) + assert snap3.nrecords() == 3 + np.testing.assert_array_equal(snap3.data_vals("x"), [1.0, 2.0, 3.0]) + np.testing.assert_array_equal(snap3.data_vals("z"), [10.0, 20.0, 30.0]) + + +def test_ddh5writer_file_structure(tmp_path): + """DDH5Writer creates /YYYY-MM-DD/-/data.ddh5 structure. + Three writers with the same name each get a separate subdirectory with a correct timestamp.""" + import datetime + import re + + dd = DataDict(x=dict(unit="m"), z=dict(axes=["x"])) + + paths = [] + for i in range(1, 4): + before = datetime.datetime.now() + with DDH5Writer(dd, basedir=tmp_path, name="myexp") as w: + w.add_data(x=float(i), z=float(i * 10)) + paths.append(w.filepath) + + for path in paths: + # file is always named data.ddh5 + assert path.name == "data.ddh5" + + # experiment name in run directory + assert "myexp" in path.parent.name + + # date folder matches YYYY-MM-DD + date_folder = path.parent.parent + assert re.match(r"\d{4}-\d{2}-\d{2}", date_folder.name) + assert date_folder.parent == tmp_path + + # timestamp in run dir name matches today's date + # format: 2026-03-02T141356_-myexp + run_dir = path.parent.name + ts_str = run_dir.split("_")[0] # e.g. 2026-03-02T141356 + ts = datetime.datetime.strptime(ts_str, "%Y-%m-%dT%H%M%S") + assert before.date() == ts.date() + + # all three run directories are distinct + assert len({p.parent for p in paths}) == 3 + + +def test_load_as_xr(tmp_path): + """load_as_xr() loads a 2D grid sweep from data.ddh5 as an xarray Dataset.""" + x_vals = np.array([0.0, 0.0, 0.0, 1.0, 1.0, 1.0]) + y_vals = np.array([0.0, 1.0, 2.0, 0.0, 1.0, 2.0]) + z_vals = x_vals * 10 + y_vals + + dd = DataDict(x=dict(unit="m"), y=dict(unit="s"), z=dict(axes=["x", "y"])) + dd.add_data(x=x_vals, y=y_vals, z=z_vals) + + with DDH5Writer(dd, basedir=tmp_path, name="test") as writer: + folder = writer.filepath.parent + + ds = load_as_xr(folder) + + assert "z" in ds + assert ds["z"].shape == (2, 3) + np.testing.assert_array_equal(ds.coords["x"].values, [0.0, 1.0]) + np.testing.assert_array_equal(ds.coords["y"].values, [0.0, 1.0, 2.0]) + + +def test_load_as_df(tmp_path): + """load_as_df() loads a sweep from data.ddh5 as a pandas DataFrame.""" + dd = DataDict(x=dict(unit="m"), z=dict(axes=["x"])) + dd.add_data(x=np.array([1.0, 2.0, 3.0]), z=np.array([10.0, 20.0, 30.0])) + + with DDH5Writer(dd, basedir=tmp_path, name="test") as writer: + folder = writer.filepath.parent + + df = load_as_df(folder) + + assert "z" in df.columns + assert df.index.names == ["x"] + np.testing.assert_array_equal(df["z"].values, [10.0, 20.0, 30.0]) + + +def test_find_data_locates_all_runs(tmp_path): + """find_data() returns all folders containing data.ddh5 under the root.""" + dd = DataDict(x=dict(unit="m"), z=dict(axes=["x"])) + + folders = [] + for i in range(1, 4): + with DDH5Writer(dd, basedir=tmp_path, name=f"run{i}") as w: + w.add_data(x=float(i), z=float(i * 10)) + folders.append(w.filepath.parent) + + found = find_data(tmp_path) + + assert len(found) == 3 + assert set(found.keys()) == set(folders) + + +def test_datadict_from_hdf5_slicing(tmp_path, simple_dd): + """datadict_from_hdf5 startidx/stopidx returns only the requested rows.""" + path = tmp_path / "data.ddh5" + datadict_to_hdf5(simple_dd, path) + + sliced = datadict_from_hdf5(path, startidx=1, stopidx=3) + + assert sliced.nrecords() == 2 + np.testing.assert_array_equal(sliced.data_vals("x"), [2.0, 3.0]) + np.testing.assert_array_equal(sliced.data_vals("z"), [20.0, 30.0]) + + +def test_find_data_folder_filter(tmp_path): + """find_data() with folder_filter returns only folders whose name matches the pattern.""" + dd = DataDict(x=dict(unit="m"), z=dict(axes=["x"])) + + with DDH5Writer(dd, basedir=tmp_path, name="cavity") as w: + w.add_data(x=1.0, z=10.0) + cavity_folder = w.filepath.parent + + with DDH5Writer(dd, basedir=tmp_path, name="qubit") as w: + w.add_data(x=2.0, z=20.0) + + found = find_data(tmp_path, folder_filter="cavity") + + assert len(found) == 1 + assert cavity_folder in found + + +def test_append_mode_none_overwrites(tmp_path, simple_dd): + """AppendMode.none deletes existing data and writes fresh on each call.""" + path = tmp_path / "data.ddh5" + + datadict_to_hdf5(simple_dd, path) + + dd_new = DataDict(x=dict(unit="m"), z=dict(axes=["x"])) + dd_new.add_data(x=np.array([9.0, 8.0]), z=np.array([90.0, 80.0])) + datadict_to_hdf5(dd_new, path, append_mode=AppendMode.none) + + dd_loaded = datadict_from_hdf5(path) + assert dd_loaded.nrecords() == 2 + np.testing.assert_array_equal(dd_loaded.data_vals("x"), [9.0, 8.0]) + np.testing.assert_array_equal(dd_loaded.data_vals("z"), [90.0, 80.0]) + + +def test_datadict_from_hdf5_structure_only(tmp_path, simple_dd): + """datadict_from_hdf5 with structure_only=True returns axes/dependents but no values.""" + path = tmp_path / "data.ddh5" + datadict_to_hdf5(simple_dd, path) + + struct = datadict_from_hdf5(path, structure_only=True) + + assert set(struct.axes()) == {"x"} + assert set(struct.dependents()) == {"z"} + assert struct.nrecords() == 0 + + +def test_hdf5_roundtrip(tmp_path, simple_dd): + """datadict_to_hdf5 + datadict_from_hdf5 preserves structure and values.""" + path = tmp_path / "data.ddh5" + + datadict_to_hdf5(simple_dd, path) + dd_loaded = datadict_from_hdf5(path) + + assert datasets_are_equal(simple_dd, dd_loaded, ignore_meta=True) + np.testing.assert_array_equal(dd_loaded.data_vals("x"), [1.0, 2.0, 3.0]) + np.testing.assert_array_equal(dd_loaded.data_vals("z"), [10.0, 20.0, 30.0]) + + +def test_all_datadicts_from_hdf5(tmp_path): + """all_datadicts_from_hdf5() loads every group from a single HDF5 file.""" + path = tmp_path / "multi.ddh5" + + dd1 = DataDict(x=dict(unit="m"), z=dict(axes=["x"])) + dd1.add_data(x=np.array([1.0, 2.0]), z=np.array([10.0, 20.0])) + + dd2 = DataDict(a=dict(unit="s"), b=dict(axes=["a"])) + dd2.add_data(a=np.array([3.0, 4.0]), b=np.array([30.0, 40.0])) + + datadict_to_hdf5(dd1, path, groupname="group1") + datadict_to_hdf5(dd2, path, groupname="group2", append_mode=AppendMode.none) + + result = all_datadicts_from_hdf5(path) + + assert set(result.keys()) == {"group1", "group2"} + np.testing.assert_array_equal(result["group1"].data_vals("x"), [1.0, 2.0]) + np.testing.assert_array_equal(result["group2"].data_vals("a"), [3.0, 4.0]) + + +def test_ddh5writer_add_tag(tmp_path): + """add_tag() creates a .tag file in the run directory.""" + dd = DataDict(x=dict(unit="m"), z=dict(axes=["x"])) + + with DDH5Writer(dd, basedir=tmp_path, name="test") as writer: + writer.add_data(x=1.0, z=10.0) + writer.add_tag("mytag") + run_dir = writer.filepath.parent + + assert (run_dir / "mytag.tag").exists() + + +def test_load_as_xr_with_fields(tmp_path): + """load_as_xr() with fields= returns only the requested dependents.""" + x_vals = np.array([0.0, 0.0, 1.0, 1.0]) + y_vals = np.array([0.0, 1.0, 0.0, 1.0]) + + dd = DataDict( + x=dict(unit="m"), + y=dict(unit="s"), + z=dict(axes=["x", "y"]), + w=dict(axes=["x", "y"]), + ) + dd.add_data(x=x_vals, y=y_vals, z=x_vals + y_vals, w=x_vals * y_vals) + + with DDH5Writer(dd, basedir=tmp_path, name="test") as writer: + folder = writer.filepath.parent + + ds = load_as_xr(folder, fields=["z"]) + + assert "z" in ds + assert "w" not in ds + + +def test_most_recent_data_path(tmp_path): + """most_recent_data_path() returns the folder with the latest timestamp.""" + dd = DataDict(x=dict(unit="m"), z=dict(axes=["x"])) + + paths = [] + for i in range(1, 4): + with DDH5Writer(dd, basedir=tmp_path, name=f"run{i}") as w: + w.add_data(x=float(i), z=float(i * 10)) + paths.append(w.filepath.parent) + + result = most_recent_data_path(tmp_path) + assert result == sorted(paths)[-1] + + +def test_find_data_newer_than_excludes_old_runs(tmp_path): + """find_data() with newer_than=now returns nothing because all runs predate the cutoff.""" + import datetime + + dd = DataDict(x=dict(unit="m"), z=dict(axes=["x"])) + for i in range(1, 3): + with DDH5Writer(dd, basedir=tmp_path, name=f"run{i}") as w: + w.add_data(x=float(i), z=float(i * 10)) + + cutoff = datetime.datetime.now() + found = find_data(tmp_path, newer_than=cutoff) + + assert len(found) == 0 + + +def test_find_data_older_than_excludes_future_runs(tmp_path): + """find_data() with older_than far in the past returns nothing because all runs postdate it.""" + import datetime + + dd = DataDict(x=dict(unit="m"), z=dict(axes=["x"])) + for i in range(1, 3): + with DDH5Writer(dd, basedir=tmp_path, name=f"run{i}") as w: + w.add_data(x=float(i), z=float(i * 10)) + + cutoff = datetime.datetime(2000, 1, 1) + found = find_data(tmp_path, older_than=cutoff) + + assert len(found) == 0 diff --git a/test/pytest/test_protocols.py b/test/pytest/test_protocols.py new file mode 100644 index 0000000..1e18bed --- /dev/null +++ b/test/pytest/test_protocols.py @@ -0,0 +1,522 @@ +""" +Unit tests for labcore.protocols.base + +Covers: ProtocolParameterBase, OperationStatus, ProtocolOperation, +SuperOperationBase, BranchBase, Condition, ProtocolBase, and the +parameter optimization lifecycle (success/retry/failure). +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from pathlib import Path + +import numpy as np +import pytest + +import labcore.protocols.base as proto_base +from labcore.protocols.base import ( + BranchBase, + Condition, + OperationStatus, + ParamImprovement, + PlatformTypes, + ProtocolBase, + ProtocolOperation, + ProtocolParameterBase, + SuperOperationBase, +) + +# --------------------------------------------------------------------------- +# Fixture: DUMMY platform for all tests, restored after each +# --------------------------------------------------------------------------- + + +@pytest.fixture(autouse=True) +def dummy_platform(): + proto_base.PLATFORMTYPE = PlatformTypes.DUMMY + yield + proto_base.PLATFORMTYPE = None + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def make_param(store: dict | None = None): + """Return a concrete ProtocolParameterBase and the dict backing it.""" + if store is None: + store = {"value": 0} + + @dataclass + class _Param(ProtocolParameterBase): + name: str = field(default="test_param", init=False) + description: str = field(default="A test parameter", init=False) + + def _dummy_getter(self): + return store["value"] + + def _dummy_setter(self, value): + store["value"] = value + + return _Param(params=None), store + + +def make_simple_op( + status: OperationStatus = OperationStatus.SUCCESS, call_log: list | None = None +): + """Return a ProtocolOperation that records execution steps and returns status.""" + log = call_log if call_log is not None else [] + + class _Op(ProtocolOperation): + def _measure_dummy(self): + log.append("measure") + return Path(".") + + def _load_data_dummy(self): + log.append("load_data") + + def analyze(self): + log.append("analyze") + + def evaluate(self) -> OperationStatus: + log.append("evaluate") + return status + + return _Op(), log + + +def make_protocol(ops, report_path: Path = Path("")): + """Minimal ProtocolBase with all ops in a single root branch.""" + + class _Proto(ProtocolBase): + def __init__(self): + super().__init__(report_path=report_path) + branch = BranchBase("root") + for op in ops: + branch.append(op) + self.root_branch = branch + + return _Proto() + + +# =========================================================================== +# 1. ProtocolParameterBase +# =========================================================================== + + +class TestProtocolParameterBase: + def test_dispatch_getter(self): + param, _ = make_param({"value": 42}) + assert param() == 42 + + def test_dispatch_setter(self): + param, store = make_param({"value": 0}) + param(99) + assert store["value"] == 99 + + def test_post_init_inherits_global_platformtype(self): + @dataclass + class _P(ProtocolParameterBase): + name: str = field(default="p", init=False) + description: str = field(default="d", init=False) + + def _dummy_getter(self): + return 1 + + def _dummy_setter(self, v): + pass + + p = _P(params=None, platform_type=None) + assert p.platform_type == PlatformTypes.DUMMY + + def test_post_init_requires_params_for_non_dummy(self): + proto_base.PLATFORMTYPE = PlatformTypes.QICK + + @dataclass + class _P(ProtocolParameterBase): + name: str = field(default="p", init=False) + description: str = field(default="d", init=False) + + with pytest.raises(ValueError): + _P(params=None) + + +# =========================================================================== +# 2. OperationStatus +# =========================================================================== + + +class TestOperationStatus: + def test_enum_values(self): + assert str(OperationStatus.SUCCESS) == "success" + assert str(OperationStatus.RETRY) == "retry" + assert str(OperationStatus.FAILURE) == "failure" + + +# =========================================================================== +# 3. ProtocolOperation +# =========================================================================== + + +class TestProtocolOperation: + def test_register_inputs_sets_attr_and_dict(self): + param, _ = make_param() + op, _ = make_simple_op() + op._register_inputs(my_param=param) + assert op.my_param is param + assert op.input_params["my_param"] is param + + def test_register_outputs_sets_attr_and_dict(self): + param, _ = make_param() + op, _ = make_simple_op() + op._register_outputs(out=param) + assert op.out is param + assert op.output_params["out"] is param + + def test_execute_calls_workflow_in_order(self): + log = [] + op, _ = make_simple_op(call_log=log) + op.execute() + assert log == ["measure", "load_data", "analyze", "evaluate"] + + def test_execute_increments_attempt_counters(self): + op, _ = make_simple_op() + op.execute() + op.execute() + assert op.current_attempt == 2 + assert op.total_attempts_made == 2 + + def test_execute_adds_retry_header_on_second_attempt(self): + op, _ = make_simple_op() + op.execute() + assert not any("ATTEMPT" in str(r) for r in op.report_output) + op.execute() + assert any("ATTEMPT 2" in str(r) for r in op.report_output) + + @pytest.mark.parametrize( + "case,expected", + [ + ("empty_independent", False), + ("matching_shapes", True), + ("mismatched_shapes", False), + ("empty_dicts", True), + ], + ) + def test_verify_shape_cases(self, case, expected): + op, _ = make_simple_op() + if case == "empty_independent": + op.independents = {"x": np.array([])} + op.dependents = {"y": np.array([1, 2])} + elif case == "matching_shapes": + op.independents = {"x": np.array([1, 2, 3])} + op.dependents = {"y": np.array([4, 5, 6])} + elif case == "mismatched_shapes": + op.independents = {"x": np.array([1, 2])} + op.dependents = {"y": np.array([1, 2, 3])} + elif case == "empty_dicts": + op.independents = {} + op.dependents = {} + assert op._verify_shape() == expected + + +# =========================================================================== +# 4. SuperOperationBase +# =========================================================================== + + +class TestSuperOperationBase: + def _make_super(self, sub_ops, evaluate_status=OperationStatus.SUCCESS): + class _Super(SuperOperationBase): + def evaluate(self) -> OperationStatus: + return evaluate_status + + s = _Super() + s.operations = sub_ops + return s + + def test_validate_rejects_condition_in_operations(self): + branch = BranchBase("b") + cond = Condition(lambda: True, branch, branch) + s = self._make_super([cond]) + with pytest.raises(ValueError, match="Condition"): + s._validate_operations() + + def test_validate_rejects_non_operation(self): + s = self._make_super(["not_an_op"]) + with pytest.raises(TypeError): + s._validate_operations() + + def test_execute_aggregates_sub_op_reports(self): + op1, _ = make_simple_op() + op1.report_output = ["op1 result"] + op2, _ = make_simple_op() + op2.report_output = ["op2 result"] + s = self._make_super([op1, op2]) + s.execute() + combined = " ".join(str(r) for r in s.report_output) + assert "op1 result" in combined + assert "op2 result" in combined + + def test_execute_returns_failure_on_sub_op_exception(self): + class _BadOp(ProtocolOperation): + def execute(self) -> OperationStatus: + raise RuntimeError("boom") + + def _measure_dummy(self): + pass + + def _load_data_dummy(self): + pass + + def analyze(self): + pass + + def evaluate(self): + return OperationStatus.SUCCESS + + s = self._make_super([_BadOp()]) + result = s.execute() + assert result == OperationStatus.FAILURE + + def test_execute_returns_failure_on_sub_op_failure(self): + op, _ = make_simple_op(status=OperationStatus.FAILURE) + s = self._make_super([op]) + result = s.execute() + assert result == OperationStatus.FAILURE + + def test_execute_calls_evaluate_at_end(self): + called = [] + + class _Super(SuperOperationBase): + def evaluate(self) -> OperationStatus: + called.append(True) + return OperationStatus.SUCCESS + + op, _ = make_simple_op() + s = _Super() + s.operations = [op] + result = s.execute() + assert called == [True] + assert result == OperationStatus.SUCCESS + + +# =========================================================================== +# 5. BranchBase +# =========================================================================== + + +class TestBranchBase: + def test_append_and_extend(self): + op1, _ = make_simple_op() + op2, _ = make_simple_op() + op3, _ = make_simple_op() + branch = BranchBase("test") + branch.append(op1).extend([op2, op3]) + assert branch.items == [op1, op2, op3] + + def test_repr(self): + op, _ = make_simple_op() + branch = BranchBase("MyBranch") + branch.append(op) + r = repr(branch) + assert "MyBranch" in r + assert "1" in r + + +# =========================================================================== +# 6. Condition +# =========================================================================== + + +class TestCondition: + def test_evaluate_true_branch(self): + true_b = BranchBase("True") + false_b = BranchBase("False") + cond = Condition(lambda: True, true_b, false_b, name="TestCond") + result = cond.evaluate() + assert result is true_b + assert cond.condition_result is True + assert cond.taken_branch is true_b + + def test_evaluate_false_branch(self): + true_b = BranchBase("True") + false_b = BranchBase("False") + cond = Condition(lambda: False, true_b, false_b, name="TestCond") + result = cond.evaluate() + assert result is false_b + assert cond.condition_result is False + assert cond.taken_branch is false_b + + def test_evaluate_appends_to_report(self): + branch = BranchBase("b") + cond = Condition(lambda: True, branch, branch, name="X") + cond.evaluate() + assert len(cond.report_output) > 0 + + +# =========================================================================== +# 7. ProtocolBase +# =========================================================================== + + +class TestProtocolBase: + def test_raises_if_platformtype_none(self): + proto_base.PLATFORMTYPE = None + with pytest.raises(ValueError, match="platform"): + make_protocol([]) + + def test_verify_all_parameters_passes(self): + op, _ = make_simple_op() + param, _ = make_param() + op._register_inputs(p=param) + proto = make_protocol([op]) + assert proto.verify_all_parameters() is True + + def test_verify_all_parameters_raises_on_bad_param(self): + @dataclass + class _BadParam(ProtocolParameterBase): + name: str = field(default="bad", init=False) + description: str = field(default="d", init=False) + + def _dummy_getter(self): + raise RuntimeError("can't reach hardware") + + def _dummy_setter(self, v): + pass + + op, _ = make_simple_op() + op._register_inputs(bad=_BadParam(params=None)) + proto = make_protocol([op]) + with pytest.raises(AttributeError): + proto.verify_all_parameters() + + def test_execute_success(self, tmp_path): + op, _ = make_simple_op(status=OperationStatus.SUCCESS) + proto = make_protocol([op], report_path=tmp_path) + proto.execute() + assert proto.success is True + + def test_execute_failure_stops_protocol(self, tmp_path): + op1, _ = make_simple_op(status=OperationStatus.FAILURE) + op2, log2 = make_simple_op() + proto = make_protocol([op1, op2], report_path=tmp_path) + proto.execute() + assert proto.success is False + assert "evaluate" not in log2 # second op never ran + + def test_execute_generates_html_report(self, tmp_path): + op, _ = make_simple_op(status=OperationStatus.SUCCESS) + proto = make_protocol([op], report_path=tmp_path) + proto.execute() + report_dir = tmp_path / f"{proto.name}_report" + html_file = report_dir / f"{proto.name}_report.html" + assert html_file.exists() + + +# =========================================================================== +# 8. Parameter optimization lifecycle +# =========================================================================== + + +class TestParameterOptimizationLifecycle: + def test_success_updates_output_parameter(self, tmp_path): + """evaluate() updates the output param and records a ParamImprovement.""" + store = {"value": 10} + + @dataclass + class _OutParam(ProtocolParameterBase): + name: str = field(default="output", init=False) + description: str = field(default="output param", init=False) + + def _dummy_getter(self): + return store["value"] + + def _dummy_setter(self, v): + store["value"] = v + + class _Op(ProtocolOperation): + def __init__(self): + super().__init__() + self._register_outputs(result=_OutParam(params=None)) + + def _measure_dummy(self): + return Path(".") + + def _load_data_dummy(self): + pass + + def analyze(self): + pass + + def evaluate(self) -> OperationStatus: + old = self.result() + new = old + 5 + self.improvements.append( + ParamImprovement(old_value=old, new_value=new, param=self.result) + ) + self.result(new) + return OperationStatus.SUCCESS + + op = _Op() + proto = make_protocol([op], report_path=tmp_path) + proto.execute() + + assert proto.success is True + assert store["value"] == 15 + assert len(op.improvements) == 1 + assert op.improvements[0].old_value == 10 + assert op.improvements[0].new_value == 15 + + def test_retry_reruns_until_success(self, tmp_path): + """RETRY on first two attempts, SUCCESS on third → protocol succeeds after 3 runs.""" + attempt = {"count": 0} + + class _Op(ProtocolOperation): + def _measure_dummy(self): + return Path(".") + + def _load_data_dummy(self): + pass + + def analyze(self): + pass + + def evaluate(self) -> OperationStatus: + attempt["count"] += 1 + if attempt["count"] < 3: + return OperationStatus.RETRY + return OperationStatus.SUCCESS + + op = _Op() + op.max_attempts = 3 + proto = make_protocol([op], report_path=tmp_path) + proto.execute() + + assert proto.success is True + assert op.total_attempts_made == 3 + + def test_retry_exhausted_marks_failure(self, tmp_path): + """Operation always returns RETRY → exhausts max_attempts → protocol fails.""" + + class _Op(ProtocolOperation): + def _measure_dummy(self): + return Path(".") + + def _load_data_dummy(self): + pass + + def analyze(self): + pass + + def evaluate(self) -> OperationStatus: + return OperationStatus.RETRY + + op = _Op() + op.max_attempts = 2 + proto = make_protocol([op], report_path=tmp_path) + proto.execute() + + assert proto.success is False + assert op.total_attempts_made == 2 diff --git a/test/pytest/test_protocols_realistic.py b/test/pytest/test_protocols_realistic.py new file mode 100644 index 0000000..b5e521d --- /dev/null +++ b/test/pytest/test_protocols_realistic.py @@ -0,0 +1,273 @@ +""" +Realistic protocol tests using the dummy operations in labcore.protocols.dummy. + +These tests exercise real curve fitting, SNR-based success/retry/failure logic, +and parameter flow between operations — as opposed to the structural unit tests in +test_protocols.py which use stub operations. + +All tests that run _measure_dummy (which calls run_and_save_sweep with a relative +"data" path) use monkeypatch.chdir(tmp_path) so that HDF5 files land under the +pytest-supplied temporary directory. +""" + +from __future__ import annotations + +import numpy as np +import pytest + +import labcore.protocols.base as proto_base +import labcore.testing.protocol_dummy.dummy_protocol as _dp_module +from labcore.measurement.record import dependent, independent, recording +from labcore.measurement.storage import run_and_save_sweep +from labcore.measurement.sweep import Sweep +from labcore.protocols.base import ( + BranchBase, + PlatformTypes, + ProtocolBase, +) +from labcore.testing.protocol_dummy import ( + CosineOperation, + DummyProtocol, + DummySuperOperation, + ExponentialDecayOperation, + ExponentialOperation, + LinearOperation, +) +from labcore.testing.protocol_dummy.gaussian import GaussianOperation + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture(autouse=True) +def dummy_platform(): + proto_base.PLATFORMTYPE = PlatformTypes.DUMMY + yield + proto_base.PLATFORMTYPE = None + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def make_protocol(ops, report_path): + """Minimal ProtocolBase with all ops in a single root branch.""" + + class _Proto(ProtocolBase): + def __init__(self): + super().__init__(report_path=report_path) + branch = BranchBase("root") + for op in ops: + branch.append(op) + self.root_branch = branch + + return _Proto() + + +# --------------------------------------------------------------------------- +# 1. GaussianProtocol — SNR-based retry until total_attempts_made == 3 +# --------------------------------------------------------------------------- + + +class TestGaussianFitWithRetry: + def test_retries_3_times_and_succeeds(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + + op = GaussianOperation() + proto = make_protocol([op], report_path=tmp_path) + proto.execute() + + assert proto.success is True + assert op.total_attempts_made == 3 + + # amplitude parameter should have been updated from its initial 0.0 + fitted_amplitude = op.amplitude() + assert fitted_amplitude is not None + assert fitted_amplitude != 0.0 + + # improvements recorded + assert len(op.improvements) == 1 + + # HTML report exists + report_dir = tmp_path / f"{proto.name}_report" + html_file = report_dir / f"{proto.name}_report.html" + assert html_file.exists() + + +# --------------------------------------------------------------------------- +# 2. ExponentialProtocol — single success, parameter updated +# --------------------------------------------------------------------------- + + +class TestExponentialFitSuccess: + def test_succeeds_and_updates_parameter(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + + op = ExponentialOperation() + proto = make_protocol([op], report_path=tmp_path) + proto.execute() + + assert proto.success is True + + # 'a' parameter was updated from initial 0.0 + fitted_a = op.a() + assert fitted_a is not None + assert fitted_a != 0.0 + + assert len(op.improvements) == 1 + + +# --------------------------------------------------------------------------- +# 3. ExponentialDecayProtocol — also exercises the fit.run() bug fix +# --------------------------------------------------------------------------- + + +class TestExponentialDecayFitSuccess: + def test_succeeds_and_updates_amplitude(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + + op = ExponentialDecayOperation() + proto = make_protocol([op], report_path=tmp_path) + proto.execute() + + assert proto.success is True + + fitted_amplitude = op.amplitude() + assert fitted_amplitude is not None + assert fitted_amplitude != 0.0 + + assert len(op.improvements) == 1 + + +# --------------------------------------------------------------------------- +# 4. LinearProtocol — single success, slope updated +# --------------------------------------------------------------------------- + + +class TestLinearFitSuccess: + def test_succeeds_and_updates_slope(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + + op = LinearOperation() + proto = make_protocol([op], report_path=tmp_path) + proto.execute() + + assert proto.success is True + + fitted_slope = op.slope() + assert fitted_slope is not None + assert fitted_slope != 0.0 + + assert len(op.improvements) == 1 + + +# --------------------------------------------------------------------------- +# 5. Sequential parameter flow: CosineProtocol then LinearProtocol +# --------------------------------------------------------------------------- + + +class TestCosineAndLinearParameterFlow: + def test_both_succeed_and_params_updated(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + + cosine_op = CosineOperation() + linear_op = LinearOperation() + proto = make_protocol([cosine_op, linear_op], report_path=tmp_path) + proto.execute() + + assert proto.success is True + + # Both output parameters updated from initial 0.0 + assert cosine_op.amplitude() != 0.0 + assert linear_op.slope() != 0.0 + + # Both recorded improvements + assert len(cosine_op.improvements) == 1 + assert len(linear_op.improvements) == 1 + + +# --------------------------------------------------------------------------- +# 6. DummyProtocol — DummySuperOperation retries 3× (accessed via root branch) +# --------------------------------------------------------------------------- + + +class TestDummySuperOperationRetries: + def test_retries_3_times_and_succeeds(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + monkeypatch.setattr(_dp_module, "USE_BRANCH_A", True) + + proto = DummyProtocol(report_path=tmp_path) + proto.execute() + + # DummySuperOperation is the second item in the main branch + super_op = proto.root_branch.items[1] + assert isinstance(super_op, DummySuperOperation) + assert proto.success is True + assert super_op.total_attempts_made == 3 + + +# --------------------------------------------------------------------------- +# 7. DummyProtocol — Condition routes to BranchA or BranchB via USE_BRANCH_A +# --------------------------------------------------------------------------- + + +class TestConditionRoutingInFullProtocol: + def test_branch_a_taken_when_flag_true(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + monkeypatch.setattr(_dp_module, "USE_BRANCH_A", True) + + proto = DummyProtocol(report_path=tmp_path) + proto.execute() + + assert proto.success is True + # Condition is the third item in the main branch + branch_condition = proto.root_branch.items[2] + assert branch_condition.taken_branch.name == "BranchA" + + def test_branch_b_taken_when_flag_false(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + monkeypatch.setattr(_dp_module, "USE_BRANCH_A", False) + monkeypatch.setattr(_dp_module, "USE_BRANCH_C", True) + + proto = DummyProtocol(report_path=tmp_path) + proto.execute() + + assert proto.success is True + branch_condition = proto.root_branch.items[2] + assert branch_condition.taken_branch.name == "BranchB" + + +# --------------------------------------------------------------------------- +# 8. Failure on bad data — nearly pure noise → SNR < threshold → FAILURE +# --------------------------------------------------------------------------- + + +class _NoisyGaussian(GaussianOperation): + """Override _measure_dummy to produce nearly pure noise (no signal).""" + + def _measure_dummy(self): + x_values = np.linspace(-10, 10, 100) + + @recording(independent("x"), dependent("y")) + def measure(x_val): + # Tiny signal swamped by large noise → SNR << threshold + return x_val, np.random.normal(0, 50.0) + + sweep = Sweep(x_values, measure) + loc, _ = run_and_save_sweep(sweep, "data", self.name) + return loc + + +class TestFailureOnBadData: + def test_noisy_data_causes_failure(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + np.random.seed(42) # deterministic noise + + op = _NoisyGaussian() + op.max_attempts = 1 # don't retry — fail fast + proto = make_protocol([op], report_path=tmp_path) + proto.execute() + + assert proto.success is False diff --git a/test/pytest/test_record.py b/test/pytest/test_record.py new file mode 100644 index 0000000..9e6efb4 --- /dev/null +++ b/test/pytest/test_record.py @@ -0,0 +1,155 @@ +import pytest + +from labcore.measurement.record import ( + DataType, + FunctionToRecords, + IteratorToRecords, + combine_data_specs, + dep, + dependent, + independent, + make_data_spec, + produces_record, + record_as, + recording, +) + + +def test_independent_creates_correct_dataspec(): + spec = independent("x", unit="m") + assert spec.name == "x" + assert spec.depends_on is None + assert spec.unit == "m" + assert spec.type == DataType.scalar + + +def test_dependent_creates_correct_dataspec(): + spec = dep("y", depends_on=["x"], unit="V") + assert spec.name == "y" + assert spec.depends_on == ["x"] + assert spec.unit == "V" + assert spec.type == DataType.scalar + + +def test_dependent_raises_when_depends_on_is_none(): + with pytest.raises(TypeError): + dependent("y", depends_on=None) + + +def test_make_data_spec_from_string(): + # A bare string creates a dependent with no explicit axes + spec = make_data_spec("z") + assert spec.name == "z" + assert spec.depends_on == [] + + +def test_make_data_spec_from_tuple(): + spec = make_data_spec(("z", ["x", "y"], "scalar", "Hz")) + assert spec.name == "z" + assert spec.depends_on == ["x", "y"] + assert spec.unit == "Hz" + + +def test_make_data_spec_from_dict(): + spec = make_data_spec({"name": "z", "depends_on": ["x"], "unit": "A"}) + assert spec.name == "z" + assert spec.depends_on == ["x"] + assert spec.unit == "A" + + +def test_make_data_spec_from_dataspec(): + original = independent("x", unit="s") + spec = make_data_spec(original) + assert spec is original + + +def test_make_data_spec_raises_on_invalid_type(): + with pytest.raises(TypeError): + make_data_spec(42) + + +def test_record_as_with_function_returns_function_to_records(): + wrapped = record_as(lambda x: x * 2, dep("y", ["x"])) + assert isinstance(wrapped, FunctionToRecords) + + +def test_function_to_records_call_returns_correct_dict(): + wrapped = record_as(lambda x: x * 2, dep("y", ["x"])) + result = wrapped(3) + assert result == {"y": 6} + + +def test_record_as_with_iterable_returns_iterator_to_records(): + wrapped = record_as(range(3), independent("x")) + assert isinstance(wrapped, IteratorToRecords) + + +def test_iterator_to_records_yields_correct_dicts(): + wrapped = record_as(range(3), independent("x")) + records = list(wrapped) + assert records == [{"x": 0}, {"x": 1}, {"x": 2}] + + +def test_produces_record_true_for_wrapped(): + wrapped = record_as(lambda x: x, "y") + assert produces_record(wrapped) is True + + +def test_produces_record_false_for_plain_function(): + assert produces_record(lambda x: x) is False + + +def test_produces_record_false_for_plain_iterable(): + assert produces_record(range(5)) is False + + +def test_function_to_records_using_prefills_args(): + wrapped = record_as(lambda x, offset: x + offset, dep("y", ["x"])) + bound = wrapped.using(offset=10) + # original is not mutated + assert wrapped._kwargs == {} + # bound version uses the pre-filled kwarg + result = bound(5) + assert result == {"y": 15} + + +def test_dataspec_repr_without_dependencies(): + spec = independent("x") + assert repr(spec) == "x" + + +def test_dataspec_repr_with_dependencies(): + spec = dep("y", depends_on=["x", "z"]) + assert repr(spec) == "y(x, z)" + + +def test_recording_decorator_wraps_function(): + @recording(dep("y", ["x"])) + def measure(x): + return x * 3 + + assert isinstance(measure, FunctionToRecords) + result = measure(4) + assert result == {"y": 12} + + +def test_recording_decorator_with_multiple_dataspecs(): + @recording(independent("x"), independent("y"), independent("z")) + def measure(t): + return t, t * 2, t * 3 + + assert isinstance(measure, FunctionToRecords) + result = measure(5) + assert result == {"x": 5, "y": 10, "z": 15} + + +def test_combine_data_specs_removes_duplicates(): + x = independent("x") + y = dep("y", ["x"]) + x_dup = independent("x", unit="m") # same name, different unit + result = combine_data_specs(x, y, x_dup) + assert len(result) == 2 + assert result[0].name == "x" + assert result[1].name == "y" + # first occurrence wins + assert result[0].unit == "" diff --git a/test/pytest/test_step_writer.py b/test/pytest/test_step_writer.py index 5897bd7..f247540 100644 --- a/test/pytest/test_step_writer.py +++ b/test/pytest/test_step_writer.py @@ -1,20 +1,24 @@ import os import shutil -from pathlib import Path from datetime import datetime +from pathlib import Path from labcore.data.datadict import DataDict, datasets_are_equal -from labcore.data.datadict_storage import DDH5Writer, datadict_to_hdf5, datadict_from_hdf5 +from labcore.data.datadict_storage import ( + DDH5Writer, + datadict_from_hdf5, + datadict_to_hdf5, +) # TODO: Add a test to see what would happen if the tmp folder gets removed mid way def test_file_creation(tmp_path, n_files=500): - datadict = DataDict(x=dict(unit='m'), y=dict(unit='m'), z=dict(axes=['x', 'y'])) + datadict = DataDict(x=dict(unit="m"), y=dict(unit="m"), z=dict(axes=["x", "y"])) with DDH5Writer(datadict, str(tmp_path), safe_write_mode=True) as writer: now = datetime.now() - + for i in range(n_files): writer.add_data(x=i, y=i**2, z=i**3) @@ -22,19 +26,19 @@ def test_file_creation(tmp_path, n_files=500): current_hour = now.strftime("%H") current_minute = now.strftime("%M") - data_tmp_path = tmp_path/writer.filepath.parent/".tmp" - today_path = data_tmp_path/today_date - hour_path = today_path/current_hour - minute_path = hour_path/current_minute + data_tmp_path = tmp_path / writer.filepath.parent / ".tmp" + today_path = data_tmp_path / today_date + hour_path = today_path / current_hour + minute_path = hour_path / current_minute assert data_tmp_path.exists() assert today_path.exists() assert hour_path.exists() assert minute_path.exists() - total_files = [file for file in data_tmp_path.rglob('*') if file.is_file()] + total_files = [file for file in data_tmp_path.rglob("*") if file.is_file()] assert len(total_files) == n_files - + def test_number_of_files_per_folder(tmp_path): @@ -43,10 +47,14 @@ def check_file_limit(root_path, max_files): Checks that the number of files does not exceed the passed max_files limit. """ root = Path(root_path) - for dirpath in root.rglob('*'): + for dirpath in root.rglob("*"): if dirpath.is_dir(): - items = list(dirpath.glob('*')) - file_count = sum(1 for item in items if item.is_file() and item.name.endswith(".ddh5")) + items = list(dirpath.glob("*")) + file_count = sum( + 1 + for item in items + if item.is_file() and item.name.endswith(".ddh5") + ) dir_count = sum(1 for item in items if item.is_dir()) if file_count > 0 and dir_count > 0: @@ -57,16 +65,18 @@ def check_file_limit(root_path, max_files): return False return True - datadict = DataDict(x=dict(unit='m'), y=dict(unit='m'), z=dict(axes=['x', 'y'])) + datadict = DataDict(x=dict(unit="m"), y=dict(unit="m"), z=dict(axes=["x", "y"])) default_n_files = DDH5Writer.n_files_per_dir n_files = 5000 DDH5Writer.n_files_per_dir = 34 - with DDH5Writer(datadict, str(tmp_path), name="first", safe_write_mode=True) as writer: + with DDH5Writer( + datadict, str(tmp_path), name="first", safe_write_mode=True + ) as writer: for i in range(n_files): - writer.add_data(x=i, y=i ** 2, z=i ** 3) + writer.add_data(x=i, y=i**2, z=i**3) data_tmp_path = tmp_path / writer.filepath.parent / ".tmp" @@ -78,11 +88,13 @@ def check_file_limit(root_path, max_files): n_files = 4206 DDH5Writer.n_files_per_dir = 12 - datadict = DataDict(x=dict(unit='m'), y=dict(unit='m'), z=dict(axes=['x', 'y'])) + datadict = DataDict(x=dict(unit="m"), y=dict(unit="m"), z=dict(axes=["x", "y"])) - with DDH5Writer(datadict, str(tmp_path), name="second", safe_write_mode=True) as writer: + with DDH5Writer( + datadict, str(tmp_path), name="second", safe_write_mode=True + ) as writer: for i in range(n_files): - writer.add_data(x=i, y=i ** 2, z=i ** 3) + writer.add_data(x=i, y=i**2, z=i**3) data_tmp_path = tmp_path / writer.filepath.parent / ".tmp" @@ -95,7 +107,7 @@ def check_file_limit(root_path, max_files): def test_basic_unification(tmp_path, n_files=500): - datadict = DataDict(x=dict(unit='m'), y=dict(unit='m'), z=dict(axes=['x', 'y'])) + datadict = DataDict(x=dict(unit="m"), y=dict(unit="m"), z=dict(axes=["x", "y"])) with DDH5Writer(datadict, str(tmp_path), safe_write_mode=True) as writer: for i in range(n_files): @@ -103,19 +115,21 @@ def test_basic_unification(tmp_path, n_files=500): data_path = writer.filepath - datadict_correct = DataDict(x=dict(unit='m'), y=dict(unit='m'), z=dict(axes=['x', 'y'])) + datadict_correct = DataDict( + x=dict(unit="m"), y=dict(unit="m"), z=dict(axes=["x", "y"]) + ) x = [] y = [] z = [] for i in range(n_files): x.append(i) - y.append(i ** 2) - z.append(i ** 3) + y.append(i**2) + z.append(i**3) datadict_correct.add_data(x=x, y=y, z=z) - correct_path = data_path.parent/"correct.ddh5" + correct_path = data_path.parent / "correct.ddh5" datadict_to_hdf5(datadict_correct, correct_path) created_data = datadict_from_hdf5(data_path) @@ -126,17 +140,19 @@ def test_basic_unification(tmp_path, n_files=500): def test_live_unification(tmp_path): - holding_path = tmp_path/"holding" + holding_path = tmp_path / "holding" holding_path.mkdir() - datadict = DataDict(x=dict(unit='m'), y=dict(unit='m'), z=dict(axes=['x', 'y'])) - datadict_correct_mid = DataDict(x=dict(unit='m'), y=dict(unit='m'), z=dict(axes=['x', 'y'])) + datadict = DataDict(x=dict(unit="m"), y=dict(unit="m"), z=dict(axes=["x", "y"])) + datadict_correct_mid = DataDict( + x=dict(unit="m"), y=dict(unit="m"), z=dict(axes=["x", "y"]) + ) x, y, z = [], [], [] for i in range(500): x.append(i) - y.append(i ** 2) - z.append(i ** 3) + y.append(i**2) + z.append(i**3) datadict_correct_mid.add_data(x=x, y=y, z=z) default_n_files_per_reconstruction = DDH5Writer.n_files_per_reconstruction @@ -152,15 +168,17 @@ def test_live_unification(tmp_path): assert datasets_are_equal(mid_point_dd, datadict_correct_mid, ignore_meta=True) assert mid_point_dd.has_meta("last_reconstructed_file") - datadict_correct_end = DataDict(x=dict(unit='m'), y=dict(unit='m'), z=dict(axes=['x', 'y'])) + datadict_correct_end = DataDict( + x=dict(unit="m"), y=dict(unit="m"), z=dict(axes=["x", "y"]) + ) for i in range(500, 1000): x.append(i) - y.append(i ** 2) - z.append(i ** 3) + y.append(i**2) + z.append(i**3) datadict_correct_end.add_data(x=x, y=y, z=z) # The .tmp inside of the data folder - tmp = data_path.parent/".tmp" + tmp = data_path.parent / ".tmp" all_files = [] for root, dirs, files in os.walk(tmp): @@ -173,29 +191,32 @@ def test_live_unification(tmp_path): # Store where the files come file_index = {} - for file in all_files[:len(all_files)-1]: + for file in all_files[: len(all_files) - 1]: file_index[file.name] = file shutil.move(file, holding_path) # End Generation for i in range(500, 1000): - writer.add_data(x=i, y=i ** 2, z=i ** 3) + writer.add_data(x=i, y=i**2, z=i**3) end_point_dd = datadict_from_hdf5(data_path) assert datasets_are_equal(end_point_dd, datadict_correct_end, ignore_meta=True) assert end_point_dd.has_meta("last_reconstructed_file") for filename, original_path in file_index.items(): - shutil.move(holding_path/filename, original_path) + shutil.move(holding_path / filename, original_path) DDH5Writer.n_files_per_reconstruction = default_n_files_per_reconstruction def test_locking_main_file(tmp_path): - datadict = DataDict(x=dict(unit='m'), y=dict(unit='m'), z=dict(axes=['x', 'y'])) + datadict = DataDict(x=dict(unit="m"), y=dict(unit="m"), z=dict(axes=["x", "y"])) - with DDH5Writer(datadict, str(tmp_path), safe_write_mode=True, file_timeout=5) as writer: + with DDH5Writer( + datadict, str(tmp_path), safe_write_mode=True, file_timeout=5 + ) as writer: + writer.n_files_per_reconstruction = 100 for i in range(500): writer.add_data(x=i, y=i**2, z=i**3) @@ -204,11 +225,11 @@ def test_locking_main_file(tmp_path): assert data_path.exists() # Making the lock file - lock_file = data_path.parent/f"~{data_path.stem}.lock" + lock_file = data_path.parent / f"~{data_path.stem}.lock" lock_file.touch() for i in range(500, 1000): - writer.add_data(x=i, y=i ** 2, z=i ** 3) + writer.add_data(x=i, y=i**2, z=i**3) assert lock_file.exists() @@ -217,15 +238,19 @@ def test_locking_main_file(tmp_path): def test_deleting_files_when_done(tmp_path): - correct_datadict = DataDict(x=dict(unit='m'), y=dict(unit='m'), z=dict(axes=['x', 'y'])) + correct_datadict = DataDict( + x=dict(unit="m"), y=dict(unit="m"), z=dict(axes=["x", "y"]) + ) for i in range(1000): - correct_datadict.add_data(x=i, y=i ** 2, z=i ** 3) + correct_datadict.add_data(x=i, y=i**2, z=i**3) - datadict = DataDict(x=dict(unit='m'), y=dict(unit='m'), z=dict(axes=['x', 'y'])) + datadict = DataDict(x=dict(unit="m"), y=dict(unit="m"), z=dict(axes=["x", "y"])) - with DDH5Writer(datadict, str(tmp_path), safe_write_mode=True, file_timeout=5) as writer: + with DDH5Writer( + datadict, str(tmp_path), safe_write_mode=True, file_timeout=5 + ) as writer: for i in range(1000): - writer.add_data(x=i, y=i ** 2, z=i ** 3) + writer.add_data(x=i, y=i**2, z=i**3) data_path = writer.filepath tmp_path = data_path.parent / ".tmp" @@ -240,24 +265,28 @@ def test_deleting_files_when_done(tmp_path): def test_deleting_files_when_done_with_lock_error(tmp_path): - correct_datadict = DataDict(x=dict(unit='m'), y=dict(unit='m'), z=dict(axes=['x', 'y'])) + correct_datadict = DataDict( + x=dict(unit="m"), y=dict(unit="m"), z=dict(axes=["x", "y"]) + ) for i in range(1000): correct_datadict.add_data(x=i, y=i**2, z=i**3) - datadict = DataDict(x=dict(unit='m'), y=dict(unit='m'), z=dict(axes=['x', 'y'])) + datadict = DataDict(x=dict(unit="m"), y=dict(unit="m"), z=dict(axes=["x", "y"])) - with DDH5Writer(datadict, str(tmp_path), safe_write_mode=True, file_timeout=5) as writer: + with DDH5Writer( + datadict, str(tmp_path), safe_write_mode=True, file_timeout=5 + ) as writer: for i in range(500): writer.add_data(x=i, y=i**2, z=i**3) data_path = writer.filepath - tmp_path = data_path.parent/".tmp" + tmp_path = data_path.parent / ".tmp" - lock_file = data_path.parent/f"~{data_path.stem}.lock" + lock_file = data_path.parent / f"~{data_path.stem}.lock" lock_file.touch() for i in range(500, 1000): - writer.add_data(x=i, y=i ** 2, z=i ** 3) + writer.add_data(x=i, y=i**2, z=i**3) assert lock_file.exists() @@ -273,26 +302,28 @@ def test_deleting_files_when_done_with_lock_error(tmp_path): def test_creation_of_not_reconstructed_error_due_to_error(tmp_path): - datadict = DataDict(x=dict(unit='m'), y=dict(unit='m'), z=dict(axes=['x', 'y'])) + datadict = DataDict(x=dict(unit="m"), y=dict(unit="m"), z=dict(axes=["x", "y"])) exception_was_raised = False try: - with DDH5Writer(datadict, str(tmp_path), safe_write_mode=True, file_timeout=5) as writer: + with DDH5Writer( + datadict, str(tmp_path), safe_write_mode=True, file_timeout=5 + ) as writer: for i in range(500): writer.add_data(x=i, y=i**2, z=i**3) data_path = writer.filepath - tmp_path = data_path.parent/".tmp" + tmp_path = data_path.parent / ".tmp" # Finds 10 files in tmp_path and deletes them files = list(tmp_path.rglob("*.ddh5"))[:10] for file in files: file.unlink() - except AssertionError as e: + except AssertionError: exception_was_raised = True assert exception_was_raised - not_reconstructed_tag = data_path.parent/"__not_reconstructed__.tag" + not_reconstructed_tag = data_path.parent / "__not_reconstructed__.tag" assert not_reconstructed_tag.exists() assert tmp_path.exists() diff --git a/test/pytest/test_sweep.py b/test/pytest/test_sweep.py new file mode 100644 index 0000000..71802f0 --- /dev/null +++ b/test/pytest/test_sweep.py @@ -0,0 +1,159 @@ +from labcore.measurement.record import dep, independent, record_as, recording +from labcore.measurement.sweep import ( + Sweep, + as_pointer, + once, + sweep_parameter, +) + + +def test_sweep_parameter_iterates_correct_number_of_steps(): + sweep = sweep_parameter("x", range(5)) + records = list(sweep) + assert len(records) == 5 + assert records == [{"x": 0}, {"x": 1}, {"x": 2}, {"x": 3}, {"x": 4}] + + +def test_sweep_parameter_record_contains_pointer_and_action_values(): + sweep = sweep_parameter("x", range(3), record_as(lambda x: x * 2, dep("y", ["x"]))) + records = list(sweep) + assert records == [{"x": 0, "y": 0}, {"x": 1, "y": 2}, {"x": 2, "y": 4}] + + +# --- Direct Sweep construction --- + + +def test_sweep_direct_construction_with_annotated_pointer(): + # Sweep(pointer) where pointer is a record_as-wrapped iterable + sweep = Sweep(record_as(range(3), independent("x"))) + records = list(sweep) + assert records == [{"x": 0}, {"x": 1}, {"x": 2}] + + +def test_sweep_direct_construction_with_pointer_and_action(): + # Sweep(pointer, action) — pointer and action both annotated + sweep = Sweep( + record_as(range(3), independent("x")), + record_as(lambda x: x**2, dep("y", ["x"])), + ) + records = list(sweep) + assert records == [{"x": 0, "y": 0}, {"x": 1, "y": 1}, {"x": 2, "y": 4}] + + +def test_sweep_direct_construction_with_multiple_actions(): + # Multiple actions are all called and merged into each record + sweep = Sweep( + record_as(range(3), independent("x")), + record_as(lambda x: x * 2, dep("y", ["x"])), + record_as(lambda x: x * 3, dep("z", ["x"])), + ) + records = list(sweep) + assert records == [ + {"x": 0, "y": 0, "z": 0}, + {"x": 1, "y": 2, "z": 3}, + {"x": 2, "y": 4, "z": 6}, + ] + + +# --- Combination operators --- + + +def test_append_operator_runs_sweeps_sequentially(): + # + : all of A, then all of B + a = sweep_parameter("x", range(3)) + b = sweep_parameter("x", range(10, 13)) + records = list(a + b) + assert records == [ + {"x": 0}, + {"x": 1}, + {"x": 2}, + {"x": 10}, + {"x": 11}, + {"x": 12}, + ] + + +def test_zip_operator_runs_sweeps_elementwise(): + # * : A and B advance together, stops at the shortest + a = sweep_parameter("x", range(3)) + b = sweep_parameter("y", range(10, 14)) # 4 elements, zip stops at 3 + records = list(a * b) + assert records == [ + {"x": 0, "y": 10}, + {"x": 1, "y": 11}, + {"x": 2, "y": 12}, + ] + + +def test_nest_operator_runs_inner_sweep_for_each_outer_step(): + # @ : full inner sweep for every outer step + outer = sweep_parameter("x", range(3)) + inner = sweep_parameter("y", range(4)) + records = list(outer @ inner) + assert records == [ + {"x": 0, "y": 0}, + {"x": 0, "y": 1}, + {"x": 0, "y": 2}, + {"x": 0, "y": 3}, + {"x": 1, "y": 0}, + {"x": 1, "y": 1}, + {"x": 1, "y": 2}, + {"x": 1, "y": 3}, + {"x": 2, "y": 0}, + {"x": 2, "y": 1}, + {"x": 2, "y": 2}, + {"x": 2, "y": 3}, + ] + + +def test_once_executes_action_exactly_once(): + call_count = 0 + + def side_effect(): + nonlocal call_count + call_count += 1 + + sweep = once(side_effect) + sweep_parameter("x", range(5)) + list(sweep) + assert call_count == 1 + + +def test_as_pointer_creates_pointer_from_generator_function(): + def gen(): + yield from range(3) + + sweep = Sweep(as_pointer(gen, independent("x"))) + records = list(sweep) + assert records == [{"x": 0}, {"x": 1}, {"x": 2}] + + +def test_all_three_operators_combined(): + @recording(dep("a", ["y"])) + def compute_a(y): + return y + 1 + + @recording(dep("b", ["z"])) + def compute_b(z): + return z * 3 + + x = Sweep(record_as(range(2), independent("x"))) + y = Sweep(record_as(range(6), independent("y")), compute_a) + z = Sweep(record_as(range(2), independent("z")), compute_b) + + records = list(x + (y @ z)) + assert records == [ + {"a": None, "b": None, "x": 0, "y": None, "z": None}, + {"a": None, "b": None, "x": 1, "y": None, "z": None}, + {"a": 1, "b": 0, "x": None, "y": 0, "z": 0}, + {"a": 1, "b": 3, "x": None, "y": 0, "z": 1}, + {"a": 2, "b": 0, "x": None, "y": 1, "z": 0}, + {"a": 2, "b": 3, "x": None, "y": 1, "z": 1}, + {"a": 3, "b": 0, "x": None, "y": 2, "z": 0}, + {"a": 3, "b": 3, "x": None, "y": 2, "z": 1}, + {"a": 4, "b": 0, "x": None, "y": 3, "z": 0}, + {"a": 4, "b": 3, "x": None, "y": 3, "z": 1}, + {"a": 5, "b": 0, "x": None, "y": 4, "z": 0}, + {"a": 5, "b": 3, "x": None, "y": 4, "z": 1}, + {"a": 6, "b": 0, "x": None, "y": 5, "z": 0}, + {"a": 6, "b": 3, "x": None, "y": 5, "z": 1}, + ] diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..b3e892a --- /dev/null +++ b/uv.lock @@ -0,0 +1,3830 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version < '3.12' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] + +[[package]] +name = "accessible-pygments" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899, upload-time = "2024-05-10T11:23:10.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" }, +] + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "appnope" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, +] + +[[package]] +name = "argon2-cffi" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi-bindings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, +] + +[[package]] +name = "argon2-cffi-bindings" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/3c0a35f46e52108d4707c44b95cfe2afcafc50800b5450c197454569b776/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f", size = 54393, upload-time = "2025-07-30T10:01:40.97Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/98bbd6ee89febd4f212696f13c03ca302b8552e7dbf9c8efa11ea4a388c3/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b", size = 29328, upload-time = "2025-07-30T10:01:41.916Z" }, + { url = "https://files.pythonhosted.org/packages/43/24/90a01c0ef12ac91a6be05969f29944643bc1e5e461155ae6559befa8f00b/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a", size = 31269, upload-time = "2025-07-30T10:01:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d3/942aa10782b2697eee7af5e12eeff5ebb325ccfb86dd8abda54174e377e4/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44", size = 86558, upload-time = "2025-07-30T10:01:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/b484f702fec5536e71836fc2dbc8c5267b3f6e78d2d539b4eaa6f0db8bf8/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb", size = 92364, upload-time = "2025-07-30T10:01:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c1/a606ff83b3f1735f3759ad0f2cd9e038a0ad11a3de3b6c673aa41c24bb7b/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92", size = 85637, upload-time = "2025-07-30T10:01:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/44/b4/678503f12aceb0262f84fa201f6027ed77d71c5019ae03b399b97caa2f19/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85", size = 91934, upload-time = "2025-07-30T10:01:47.203Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c7/f36bd08ef9bd9f0a9cff9428406651f5937ce27b6c5b07b92d41f91ae541/argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f", size = 28158, upload-time = "2025-07-30T10:01:48.341Z" }, + { url = "https://files.pythonhosted.org/packages/b3/80/0106a7448abb24a2c467bf7d527fe5413b7fdfa4ad6d6a96a43a62ef3988/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6", size = 32597, upload-time = "2025-07-30T10:01:49.112Z" }, + { url = "https://files.pythonhosted.org/packages/05/b8/d663c9caea07e9180b2cb662772865230715cbd573ba3b5e81793d580316/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623", size = 28231, upload-time = "2025-07-30T10:01:49.92Z" }, + { url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" }, + { url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" }, + { url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" }, + { url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" }, + { url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" }, + { url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, +] + +[[package]] +name = "arrow" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/33/032cdc44182491aa708d06a68b62434140d8c50820a087fac7af37703357/arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7", size = 152931, upload-time = "2025-10-18T17:46:46.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", size = 68797, upload-time = "2025-10-18T17:46:45.663Z" }, +] + +[[package]] +name = "asteval" +version = "1.0.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/d4/c19cac7814b8ec273804ebee3c5d3c69ee2084cb75f25297cb4177a6aa85/asteval-1.0.8.tar.gz", hash = "sha256:7175134331726df0e1569f4ab5fa59266192cf1b365db0ff463c978842075cbb", size = 53989, upload-time = "2025-12-17T20:56:08.383Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/31/6cf181011dc738c33bf6ba7aea2e8e1d3c1f71b7dab1942f3054f66f6202/asteval-1.0.8-py3-none-any.whl", hash = "sha256:6c64385c6ff859a474953c124987c7ee8354d781c76509b2c598741c4d1d28e9", size = 22968, upload-time = "2025-12-17T20:56:07.457Z" }, +] + +[[package]] +name = "asttokens" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, +] + +[[package]] +name = "async-lru" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/c3/bbf34f15ea88dfb649ab2c40f9d75081784a50573a9ea431563cab64adb8/async_lru-2.1.0.tar.gz", hash = "sha256:9eeb2fecd3fe42cc8a787fc32ead53a3a7158cc43d039c3c55ab3e4e5b2a80ed", size = 12041, upload-time = "2026-01-17T22:52:18.931Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/e9/eb6a5db5ac505d5d45715388e92bced7a5bb556facc4d0865d192823f2d2/async_lru-2.1.0-py3-none-any.whl", hash = "sha256:fa12dcf99a42ac1280bc16c634bbaf06883809790f6304d85cdab3f666f33a7e", size = 6933, upload-time = "2026-01-17T22:52:17.389Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, +] + +[[package]] +name = "bleach" +version = "6.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/18/3c8523962314be6bf4c8989c79ad9531c825210dd13a8669f6b84336e8bd/bleach-6.3.0.tar.gz", hash = "sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22", size = 203533, upload-time = "2025-10-27T17:57:39.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl", hash = "sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6", size = 164437, upload-time = "2025-10-27T17:57:37.538Z" }, +] + +[package.optional-dependencies] +css = [ + { name = "tinycss2" }, +] + +[[package]] +name = "bokeh" +version = "3.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "jinja2" }, + { name = "narwhals" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "pyyaml" }, + { name = "tornado", marker = "sys_platform != 'emscripten'" }, + { name = "xyzservices" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/31/7ee0c4dfd0255631b0624ce01be178704f91f763f02a1879368eb109befd/bokeh-3.8.2.tar.gz", hash = "sha256:8e7dcacc21d53905581b54328ad2705954f72f2997f99fc332c1de8da53aa3cc", size = 6529251, upload-time = "2026-01-06T00:20:06.568Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/a8/877f306720bc114c612579c5af36bcb359026b83d051226945499b306b1a/bokeh-3.8.2-py3-none-any.whl", hash = "sha256:5e2c0d84f75acb25d60efb9e4d2f434a791c4639b47d685534194c4e07bd0111", size = 7207131, upload-time = "2026-01-06T00:20:04.917Z" }, +] + +[[package]] +name = "broadbean" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib" }, + { name = "numpy" }, + { name = "schema" }, + { name = "versioningit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/c9/c64ec69941544561f503fe092f2f32b9117252b2dd22b90368787d2316a2/broadbean-0.14.0.tar.gz", hash = "sha256:bfe3afea69529da246f7ca2803d0213c625f96b15a7ca4283b9c22f8fc5c655c", size = 44803, upload-time = "2024-03-06T22:15:44.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/4d/2b3b4b35456176182d45cdf977fdea80bf71be563f4074536ec3436eed9c/broadbean-0.14.0-py3-none-any.whl", hash = "sha256:7a9195ef16241853e2ea20aedc6f67ee72f5464a463b3584fcbedcb63daf88e7", size = 36755, upload-time = "2024-03-06T22:15:42.41Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "cf-xarray" +version = "0.10.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "xarray" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/78/f4f38e7ea6221773ea48d85c00d529b1fdc7378a1a1b77c2b77661446a0b/cf_xarray-0.10.11.tar.gz", hash = "sha256:e10ee37b0ed3ba36f42346360f2bc070c690ddc73bb9dcdd9463b3a221453be3", size = 686693, upload-time = "2026-02-03T19:17:42.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/ce/5c4f4660da5521d90bea62cdf8396d7e4ce4a00513e218d267b97f9ea453/cf_xarray-0.10.11-py3-none-any.whl", hash = "sha256:c47fff625766c69a66fedef368d9787acb0819b32d8bd022f8b045089b42109a", size = 78421, upload-time = "2026-02-03T19:17:40.431Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "colorcet" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/c3/ae78e10b7139d6b7ce080d2e81d822715763336aa4229720f49cb3b3e15b/colorcet-3.1.0.tar.gz", hash = "sha256:2921b3cd81a2288aaf2d63dbc0ce3c26dcd882e8c389cc505d6886bf7aa9a4eb", size = 2183107, upload-time = "2024-02-29T19:15:42.976Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/c6/9963d588cc3d75d766c819e0377a168ef83cf3316a92769971527a1ad1de/colorcet-3.1.0-py3-none-any.whl", hash = "sha256:2a7d59cc8d0f7938eeedd08aad3152b5319b4ba3bcb7a612398cc17a384cb296", size = 260286, upload-time = "2024-02-29T19:15:40.494Z" }, +] + +[[package]] +name = "comm" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" }, + { url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" }, + { url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" }, + { url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114, upload-time = "2026-02-09T12:56:35.749Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220, upload-time = "2026-02-09T12:56:37.796Z" }, + { url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164, upload-time = "2026-02-09T12:56:39.016Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325, upload-time = "2026-02-09T12:56:40.668Z" }, + { url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913, upload-time = "2026-02-09T12:56:41.97Z" }, + { url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974, upload-time = "2026-02-09T12:56:43.323Z" }, + { url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741, upload-time = "2026-02-09T12:56:45.155Z" }, + { url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695, upload-time = "2026-02-09T12:56:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599, upload-time = "2026-02-09T12:56:48.13Z" }, + { url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780, upload-time = "2026-02-09T12:56:50.479Z" }, + { url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715, upload-time = "2026-02-09T12:56:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385, upload-time = "2026-02-09T12:56:53.194Z" }, + { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" }, + { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" }, + { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" }, + { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" }, + { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" }, + { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" }, + { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" }, + { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" }, + { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" }, + { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" }, + { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" }, + { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" }, + { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" }, + { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" }, + { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" }, + { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" }, + { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" }, + { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" }, + { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" }, + { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" }, + { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" }, + { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" }, + { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" }, + { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" }, + { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" }, + { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" }, + { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" }, + { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" }, + { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" }, + { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" }, + { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" }, + { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" }, + { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" }, + { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" }, + { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "dask" +version = "2026.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "cloudpickle" }, + { name = "fsspec" }, + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "packaging" }, + { name = "partd" }, + { name = "pyyaml" }, + { name = "toolz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/52/b0f9172b22778def907db1ff173249e4eb41f054b46a9c83b1528aaf811f/dask-2026.1.2.tar.gz", hash = "sha256:1136683de2750d98ea792670f7434e6c1cfce90cab2cc2f2495a9e60fd25a4fc", size = 10997838, upload-time = "2026-01-30T21:04:20.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/23/d39ccc4ed76222db31530b0a7d38876fdb7673e23f838e8d8f0ed4651a4f/dask-2026.1.2-py3-none-any.whl", hash = "sha256:46a0cf3b8d87f78a3d2e6b145aea4418a6d6d606fe6a16c79bd8ca2bb862bc91", size = 1482084, upload-time = "2026-01-30T21:04:18.363Z" }, +] + +[[package]] +name = "debugpy" +version = "1.8.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/cd8080344452e4874aae67c40d8940e2b4d47b01601a8fd9f44786c757c7/debugpy-1.8.20.tar.gz", hash = "sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33", size = 1645207, upload-time = "2026-01-29T23:03:28.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/56/c3baf5cbe4dd77427fd9aef99fcdade259ad128feeb8a786c246adb838e5/debugpy-1.8.20-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:eada6042ad88fa1571b74bd5402ee8b86eded7a8f7b827849761700aff171f1b", size = 2208318, upload-time = "2026-01-29T23:03:36.481Z" }, + { url = "https://files.pythonhosted.org/packages/9a/7d/4fa79a57a8e69fe0d9763e98d1110320f9ecd7f1f362572e3aafd7417c9d/debugpy-1.8.20-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:7de0b7dfeedc504421032afba845ae2a7bcc32ddfb07dae2c3ca5442f821c344", size = 3171493, upload-time = "2026-01-29T23:03:37.775Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f2/1e8f8affe51e12a26f3a8a8a4277d6e60aa89d0a66512f63b1e799d424a4/debugpy-1.8.20-cp311-cp311-win32.whl", hash = "sha256:773e839380cf459caf73cc533ea45ec2737a5cc184cf1b3b796cd4fd98504fec", size = 5209240, upload-time = "2026-01-29T23:03:39.109Z" }, + { url = "https://files.pythonhosted.org/packages/d5/92/1cb532e88560cbee973396254b21bece8c5d7c2ece958a67afa08c9f10dc/debugpy-1.8.20-cp311-cp311-win_amd64.whl", hash = "sha256:1f7650546e0eded1902d0f6af28f787fa1f1dbdbc97ddabaf1cd963a405930cb", size = 5233481, upload-time = "2026-01-29T23:03:40.659Z" }, + { url = "https://files.pythonhosted.org/packages/14/57/7f34f4736bfb6e00f2e4c96351b07805d83c9a7b33d28580ae01374430f7/debugpy-1.8.20-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ae3135e2089905a916909ef31922b2d733d756f66d87345b3e5e52b7a55f13d", size = 2550686, upload-time = "2026-01-29T23:03:42.023Z" }, + { url = "https://files.pythonhosted.org/packages/ab/78/b193a3975ca34458f6f0e24aaf5c3e3da72f5401f6054c0dfd004b41726f/debugpy-1.8.20-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:88f47850a4284b88bd2bfee1f26132147d5d504e4e86c22485dfa44b97e19b4b", size = 4310588, upload-time = "2026-01-29T23:03:43.314Z" }, + { url = "https://files.pythonhosted.org/packages/c1/55/f14deb95eaf4f30f07ef4b90a8590fc05d9e04df85ee379712f6fb6736d7/debugpy-1.8.20-cp312-cp312-win32.whl", hash = "sha256:4057ac68f892064e5f98209ab582abfee3b543fb55d2e87610ddc133a954d390", size = 5331372, upload-time = "2026-01-29T23:03:45.526Z" }, + { url = "https://files.pythonhosted.org/packages/a1/39/2bef246368bd42f9bd7cba99844542b74b84dacbdbea0833e610f384fee8/debugpy-1.8.20-cp312-cp312-win_amd64.whl", hash = "sha256:a1a8f851e7cf171330679ef6997e9c579ef6dd33c9098458bd9986a0f4ca52e3", size = 5372835, upload-time = "2026-01-29T23:03:47.245Z" }, + { url = "https://files.pythonhosted.org/packages/15/e2/fc500524cc6f104a9d049abc85a0a8b3f0d14c0a39b9c140511c61e5b40b/debugpy-1.8.20-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:5dff4bb27027821fdfcc9e8f87309a28988231165147c31730128b1c983e282a", size = 2539560, upload-time = "2026-01-29T23:03:48.738Z" }, + { url = "https://files.pythonhosted.org/packages/90/83/fb33dcea789ed6018f8da20c5a9bc9d82adc65c0c990faed43f7c955da46/debugpy-1.8.20-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:84562982dd7cf5ebebfdea667ca20a064e096099997b175fe204e86817f64eaf", size = 4293272, upload-time = "2026-01-29T23:03:50.169Z" }, + { url = "https://files.pythonhosted.org/packages/a6/25/b1e4a01bfb824d79a6af24b99ef291e24189080c93576dfd9b1a2815cd0f/debugpy-1.8.20-cp313-cp313-win32.whl", hash = "sha256:da11dea6447b2cadbf8ce2bec59ecea87cc18d2c574980f643f2d2dfe4862393", size = 5331208, upload-time = "2026-01-29T23:03:51.547Z" }, + { url = "https://files.pythonhosted.org/packages/13/f7/a0b368ce54ffff9e9028c098bd2d28cfc5b54f9f6c186929083d4c60ba58/debugpy-1.8.20-cp313-cp313-win_amd64.whl", hash = "sha256:eb506e45943cab2efb7c6eafdd65b842f3ae779f020c82221f55aca9de135ed7", size = 5372930, upload-time = "2026-01-29T23:03:53.585Z" }, + { url = "https://files.pythonhosted.org/packages/33/2e/f6cb9a8a13f5058f0a20fe09711a7b726232cd5a78c6a7c05b2ec726cff9/debugpy-1.8.20-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9c74df62fc064cd5e5eaca1353a3ef5a5d50da5eb8058fcef63106f7bebe6173", size = 2538066, upload-time = "2026-01-29T23:03:54.999Z" }, + { url = "https://files.pythonhosted.org/packages/c5/56/6ddca50b53624e1ca3ce1d1e49ff22db46c47ea5fb4c0cc5c9b90a616364/debugpy-1.8.20-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:077a7447589ee9bc1ff0cdf443566d0ecf540ac8aa7333b775ebcb8ce9f4ecad", size = 4269425, upload-time = "2026-01-29T23:03:56.518Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d9/d64199c14a0d4c476df46c82470a3ce45c8d183a6796cfb5e66533b3663c/debugpy-1.8.20-cp314-cp314-win32.whl", hash = "sha256:352036a99dd35053b37b7803f748efc456076f929c6a895556932eaf2d23b07f", size = 5331407, upload-time = "2026-01-29T23:03:58.481Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d9/1f07395b54413432624d61524dfd98c1a7c7827d2abfdb8829ac92638205/debugpy-1.8.20-cp314-cp314-win_amd64.whl", hash = "sha256:a98eec61135465b062846112e5ecf2eebb855305acc1dfbae43b72903b8ab5be", size = 5372521, upload-time = "2026-01-29T23:03:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658, upload-time = "2026-01-29T23:04:17.404Z" }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "dill" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, +] + +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + +[[package]] +name = "fastjsonschema" +version = "2.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de", size = 374130, upload-time = "2025-08-14T18:49:36.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, +] + +[[package]] +name = "fonttools" +version = "4.61.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756, upload-time = "2025-12-12T17:31:24.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/12/bf9f4eaa2fad039356cc627587e30ed008c03f1cebd3034376b5ee8d1d44/fonttools-4.61.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09", size = 2852213, upload-time = "2025-12-12T17:29:46.675Z" }, + { url = "https://files.pythonhosted.org/packages/ac/49/4138d1acb6261499bedde1c07f8c2605d1d8f9d77a151e5507fd3ef084b6/fonttools-4.61.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37", size = 2401689, upload-time = "2025-12-12T17:29:48.769Z" }, + { url = "https://files.pythonhosted.org/packages/e5/fe/e6ce0fe20a40e03aef906af60aa87668696f9e4802fa283627d0b5ed777f/fonttools-4.61.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb", size = 5058809, upload-time = "2025-12-12T17:29:51.701Z" }, + { url = "https://files.pythonhosted.org/packages/79/61/1ca198af22f7dd22c17ab86e9024ed3c06299cfdb08170640e9996d501a0/fonttools-4.61.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9", size = 5036039, upload-time = "2025-12-12T17:29:53.659Z" }, + { url = "https://files.pythonhosted.org/packages/99/cc/fa1801e408586b5fce4da9f5455af8d770f4fc57391cd5da7256bb364d38/fonttools-4.61.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87", size = 5034714, upload-time = "2025-12-12T17:29:55.592Z" }, + { url = "https://files.pythonhosted.org/packages/bf/aa/b7aeafe65adb1b0a925f8f25725e09f078c635bc22754f3fecb7456955b0/fonttools-4.61.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56", size = 5158648, upload-time = "2025-12-12T17:29:57.861Z" }, + { url = "https://files.pythonhosted.org/packages/99/f9/08ea7a38663328881384c6e7777bbefc46fd7d282adfd87a7d2b84ec9d50/fonttools-4.61.1-cp311-cp311-win32.whl", hash = "sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a", size = 2280681, upload-time = "2025-12-12T17:29:59.943Z" }, + { url = "https://files.pythonhosted.org/packages/07/ad/37dd1ae5fa6e01612a1fbb954f0927681f282925a86e86198ccd7b15d515/fonttools-4.61.1-cp311-cp311-win_amd64.whl", hash = "sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7", size = 2331951, upload-time = "2025-12-12T17:30:02.254Z" }, + { url = "https://files.pythonhosted.org/packages/6f/16/7decaa24a1bd3a70c607b2e29f0adc6159f36a7e40eaba59846414765fd4/fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e", size = 2851593, upload-time = "2025-12-12T17:30:04.225Z" }, + { url = "https://files.pythonhosted.org/packages/94/98/3c4cb97c64713a8cf499b3245c3bf9a2b8fd16a3e375feff2aed78f96259/fonttools-4.61.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2", size = 2400231, upload-time = "2025-12-12T17:30:06.47Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/82dbef0f6342eb01f54bca073ac1498433d6ce71e50c3c3282b655733b31/fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796", size = 4954103, upload-time = "2025-12-12T17:30:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/6c/44/f3aeac0fa98e7ad527f479e161aca6c3a1e47bb6996b053d45226fe37bf2/fonttools-4.61.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d", size = 5004295, upload-time = "2025-12-12T17:30:10.56Z" }, + { url = "https://files.pythonhosted.org/packages/14/e8/7424ced75473983b964d09f6747fa09f054a6d656f60e9ac9324cf40c743/fonttools-4.61.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8", size = 4944109, upload-time = "2025-12-12T17:30:12.874Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8b/6391b257fa3d0b553d73e778f953a2f0154292a7a7a085e2374b111e5410/fonttools-4.61.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0", size = 5093598, upload-time = "2025-12-12T17:30:15.79Z" }, + { url = "https://files.pythonhosted.org/packages/d9/71/fd2ea96cdc512d92da5678a1c98c267ddd4d8c5130b76d0f7a80f9a9fde8/fonttools-4.61.1-cp312-cp312-win32.whl", hash = "sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261", size = 2269060, upload-time = "2025-12-12T17:30:18.058Z" }, + { url = "https://files.pythonhosted.org/packages/80/3b/a3e81b71aed5a688e89dfe0e2694b26b78c7d7f39a5ffd8a7d75f54a12a8/fonttools-4.61.1-cp312-cp312-win_amd64.whl", hash = "sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9", size = 2319078, upload-time = "2025-12-12T17:30:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/4b/cf/00ba28b0990982530addb8dc3e9e6f2fa9cb5c20df2abdda7baa755e8fe1/fonttools-4.61.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c", size = 2846454, upload-time = "2025-12-12T17:30:24.938Z" }, + { url = "https://files.pythonhosted.org/packages/5a/ca/468c9a8446a2103ae645d14fee3f610567b7042aba85031c1c65e3ef7471/fonttools-4.61.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e", size = 2398191, upload-time = "2025-12-12T17:30:27.343Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4b/d67eedaed19def5967fade3297fed8161b25ba94699efc124b14fb68cdbc/fonttools-4.61.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5", size = 4928410, upload-time = "2025-12-12T17:30:29.771Z" }, + { url = "https://files.pythonhosted.org/packages/b0/8d/6fb3494dfe61a46258cd93d979cf4725ded4eb46c2a4ca35e4490d84daea/fonttools-4.61.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd", size = 4984460, upload-time = "2025-12-12T17:30:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/f7/f1/a47f1d30b3dc00d75e7af762652d4cbc3dff5c2697a0dbd5203c81afd9c3/fonttools-4.61.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3", size = 4925800, upload-time = "2025-12-12T17:30:34.339Z" }, + { url = "https://files.pythonhosted.org/packages/a7/01/e6ae64a0981076e8a66906fab01539799546181e32a37a0257b77e4aa88b/fonttools-4.61.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d", size = 5067859, upload-time = "2025-12-12T17:30:36.593Z" }, + { url = "https://files.pythonhosted.org/packages/73/aa/28e40b8d6809a9b5075350a86779163f074d2b617c15d22343fce81918db/fonttools-4.61.1-cp313-cp313-win32.whl", hash = "sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c", size = 2267821, upload-time = "2025-12-12T17:30:38.478Z" }, + { url = "https://files.pythonhosted.org/packages/1a/59/453c06d1d83dc0951b69ef692d6b9f1846680342927df54e9a1ca91c6f90/fonttools-4.61.1-cp313-cp313-win_amd64.whl", hash = "sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b", size = 2318169, upload-time = "2025-12-12T17:30:40.951Z" }, + { url = "https://files.pythonhosted.org/packages/32/8f/4e7bf82c0cbb738d3c2206c920ca34ca74ef9dabde779030145d28665104/fonttools-4.61.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd", size = 2846094, upload-time = "2025-12-12T17:30:43.511Z" }, + { url = "https://files.pythonhosted.org/packages/71/09/d44e45d0a4f3a651f23a1e9d42de43bc643cce2971b19e784cc67d823676/fonttools-4.61.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e", size = 2396589, upload-time = "2025-12-12T17:30:45.681Z" }, + { url = "https://files.pythonhosted.org/packages/89/18/58c64cafcf8eb677a99ef593121f719e6dcbdb7d1c594ae5a10d4997ca8a/fonttools-4.61.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c", size = 4877892, upload-time = "2025-12-12T17:30:47.709Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ec/9e6b38c7ba1e09eb51db849d5450f4c05b7e78481f662c3b79dbde6f3d04/fonttools-4.61.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75", size = 4972884, upload-time = "2025-12-12T17:30:49.656Z" }, + { url = "https://files.pythonhosted.org/packages/5e/87/b5339da8e0256734ba0dbbf5b6cdebb1dd79b01dc8c270989b7bcd465541/fonttools-4.61.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063", size = 4924405, upload-time = "2025-12-12T17:30:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/0b/47/e3409f1e1e69c073a3a6fd8cb886eb18c0bae0ee13db2c8d5e7f8495e8b7/fonttools-4.61.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2", size = 5035553, upload-time = "2025-12-12T17:30:54.823Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b6/1f6600161b1073a984294c6c031e1a56ebf95b6164249eecf30012bb2e38/fonttools-4.61.1-cp314-cp314-win32.whl", hash = "sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c", size = 2271915, upload-time = "2025-12-12T17:30:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/52/7b/91e7b01e37cc8eb0e1f770d08305b3655e4f002fc160fb82b3390eabacf5/fonttools-4.61.1-cp314-cp314-win_amd64.whl", hash = "sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c", size = 2323487, upload-time = "2025-12-12T17:30:59.804Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/908ad78e46c61c3e3ed70c3b58ff82ab48437faf84ec84f109592cabbd9f/fonttools-4.61.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa", size = 2929571, upload-time = "2025-12-12T17:31:02.574Z" }, + { url = "https://files.pythonhosted.org/packages/bd/41/975804132c6dea64cdbfbaa59f3518a21c137a10cccf962805b301ac6ab2/fonttools-4.61.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91", size = 2435317, upload-time = "2025-12-12T17:31:04.974Z" }, + { url = "https://files.pythonhosted.org/packages/b0/5a/aef2a0a8daf1ebaae4cfd83f84186d4a72ee08fd6a8451289fcd03ffa8a4/fonttools-4.61.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19", size = 4882124, upload-time = "2025-12-12T17:31:07.456Z" }, + { url = "https://files.pythonhosted.org/packages/80/33/d6db3485b645b81cea538c9d1c9219d5805f0877fda18777add4671c5240/fonttools-4.61.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba", size = 5100391, upload-time = "2025-12-12T17:31:09.732Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d6/675ba631454043c75fcf76f0ca5463eac8eb0666ea1d7badae5fea001155/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7", size = 4978800, upload-time = "2025-12-12T17:31:11.681Z" }, + { url = "https://files.pythonhosted.org/packages/7f/33/d3ec753d547a8d2bdaedd390d4a814e8d5b45a093d558f025c6b990b554c/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118", size = 5006426, upload-time = "2025-12-12T17:31:13.764Z" }, + { url = "https://files.pythonhosted.org/packages/b4/40/cc11f378b561a67bea850ab50063366a0d1dd3f6d0a30ce0f874b0ad5664/fonttools-4.61.1-cp314-cp314t-win32.whl", hash = "sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5", size = 2335377, upload-time = "2025-12-12T17:31:16.49Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ff/c9a2b66b39f8628531ea58b320d66d951267c98c6a38684daa8f50fb02f8/fonttools-4.61.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b", size = 2400613, upload-time = "2025-12-12T17:31:18.769Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" }, +] + +[[package]] +name = "fqdn" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/3e/a80a8c077fd798951169626cde3e239adeba7dab75deb3555716415bd9b0/fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f", size = 6015, upload-time = "2021-03-11T07:16:29.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, +] + +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, +] + +[[package]] +name = "gitpython" +version = "3.1.46" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/b5/59d16470a1f0dfe8c793f9ef56fd3826093fc52b3bd96d6b9d6c26c7e27b/gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f", size = 215371, upload-time = "2026-01-01T15:37:32.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620, upload-time = "2026-01-01T15:37:30.574Z" }, +] + +[[package]] +name = "greenlet" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/99/1cd3411c56a410994669062bd73dd58270c00cc074cac15f385a1fd91f8a/greenlet-3.3.1.tar.gz", hash = "sha256:41848f3230b58c08bb43dee542e74a2a2e34d3c59dc3076cec9151aeeedcae98", size = 184690, upload-time = "2026-01-23T15:31:02.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/e8/2e1462c8fdbe0f210feb5ac7ad2d9029af8be3bf45bd9fa39765f821642f/greenlet-3.3.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5fd23b9bc6d37b563211c6abbb1b3cab27db385a4449af5c32e932f93017080c", size = 274974, upload-time = "2026-01-23T15:31:02.891Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a8/530a401419a6b302af59f67aaf0b9ba1015855ea7e56c036b5928793c5bd/greenlet-3.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f51496a0bfbaa9d74d36a52d2580d1ef5ed4fdfcff0a73730abfbbbe1403dd", size = 577175, upload-time = "2026-01-23T16:00:56.213Z" }, + { url = "https://files.pythonhosted.org/packages/8e/89/7e812bb9c05e1aaef9b597ac1d0962b9021d2c6269354966451e885c4e6b/greenlet-3.3.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb0feb07fe6e6a74615ee62a880007d976cf739b6669cce95daa7373d4fc69c5", size = 590401, upload-time = "2026-01-23T16:05:26.365Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/e2d5f0e59b94a2269b68a629173263fa40b63da32f5c231307c349315871/greenlet-3.3.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67ea3fc73c8cd92f42467a72b75e8f05ed51a0e9b1d15398c913416f2dafd49f", size = 601161, upload-time = "2026-01-23T16:15:53.456Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ae/8d472e1f5ac5efe55c563f3eabb38c98a44b832602e12910750a7c025802/greenlet-3.3.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39eda9ba259cc9801da05351eaa8576e9aa83eb9411e8f0c299e05d712a210f2", size = 590272, upload-time = "2026-01-23T15:32:49.411Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/0fde34bebfcadc833550717eade64e35ec8738e6b097d5d248274a01258b/greenlet-3.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e2e7e882f83149f0a71ac822ebf156d902e7a5d22c9045e3e0d1daf59cee2cc9", size = 1550729, upload-time = "2026-01-23T16:04:20.867Z" }, + { url = "https://files.pythonhosted.org/packages/16/c9/2fb47bee83b25b119d5a35d580807bb8b92480a54b68fef009a02945629f/greenlet-3.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80aa4d79eb5564f2e0a6144fcc744b5a37c56c4a92d60920720e99210d88db0f", size = 1615552, upload-time = "2026-01-23T15:33:45.743Z" }, + { url = "https://files.pythonhosted.org/packages/1f/54/dcf9f737b96606f82f8dd05becfb8d238db0633dd7397d542a296fe9cad3/greenlet-3.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:32e4ca9777c5addcbf42ff3915d99030d8e00173a56f80001fb3875998fe410b", size = 226462, upload-time = "2026-01-23T15:36:50.422Z" }, + { url = "https://files.pythonhosted.org/packages/91/37/61e1015cf944ddd2337447d8e97fb423ac9bc21f9963fb5f206b53d65649/greenlet-3.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:da19609432f353fed186cc1b85e9440db93d489f198b4bdf42ae19cc9d9ac9b4", size = 225715, upload-time = "2026-01-23T15:33:17.298Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c8/9d76a66421d1ae24340dfae7e79c313957f6e3195c144d2c73333b5bfe34/greenlet-3.3.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7e806ca53acf6d15a888405880766ec84721aa4181261cd11a457dfe9a7a4975", size = 276443, upload-time = "2026-01-23T15:30:10.066Z" }, + { url = "https://files.pythonhosted.org/packages/81/99/401ff34bb3c032d1f10477d199724f5e5f6fbfb59816ad1455c79c1eb8e7/greenlet-3.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d842c94b9155f1c9b3058036c24ffb8ff78b428414a19792b2380be9cecf4f36", size = 597359, upload-time = "2026-01-23T16:00:57.394Z" }, + { url = "https://files.pythonhosted.org/packages/2b/bc/4dcc0871ed557792d304f50be0f7487a14e017952ec689effe2180a6ff35/greenlet-3.3.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20fedaadd422fa02695f82093f9a98bad3dab5fcda793c658b945fcde2ab27ba", size = 607805, upload-time = "2026-01-23T16:05:28.068Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cd/7a7ca57588dac3389e97f7c9521cb6641fd8b6602faf1eaa4188384757df/greenlet-3.3.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c620051669fd04ac6b60ebc70478210119c56e2d5d5df848baec4312e260e4ca", size = 622363, upload-time = "2026-01-23T16:15:54.754Z" }, + { url = "https://files.pythonhosted.org/packages/cf/05/821587cf19e2ce1f2b24945d890b164401e5085f9d09cbd969b0c193cd20/greenlet-3.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14194f5f4305800ff329cbf02c5fcc88f01886cadd29941b807668a45f0d2336", size = 609947, upload-time = "2026-01-23T15:32:51.004Z" }, + { url = "https://files.pythonhosted.org/packages/a4/52/ee8c46ed9f8babaa93a19e577f26e3d28a519feac6350ed6f25f1afee7e9/greenlet-3.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7b2fe4150a0cf59f847a67db8c155ac36aed89080a6a639e9f16df5d6c6096f1", size = 1567487, upload-time = "2026-01-23T16:04:22.125Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7c/456a74f07029597626f3a6db71b273a3632aecb9afafeeca452cfa633197/greenlet-3.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49f4ad195d45f4a66a0eb9c1ba4832bb380570d361912fa3554746830d332149", size = 1636087, upload-time = "2026-01-23T15:33:47.486Z" }, + { url = "https://files.pythonhosted.org/packages/34/2f/5e0e41f33c69655300a5e54aeb637cf8ff57f1786a3aba374eacc0228c1d/greenlet-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:cc98b9c4e4870fa983436afa999d4eb16b12872fab7071423d5262fa7120d57a", size = 227156, upload-time = "2026-01-23T15:34:34.808Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ab/717c58343cf02c5265b531384b248787e04d8160b8afe53d9eec053d7b44/greenlet-3.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bfb2d1763d777de5ee495c85309460f6fd8146e50ec9d0ae0183dbf6f0a829d1", size = 226403, upload-time = "2026-01-23T15:31:39.372Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ab/d26750f2b7242c2b90ea2ad71de70cfcd73a948a49513188a0fc0d6fc15a/greenlet-3.3.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:7ab327905cabb0622adca5971e488064e35115430cec2c35a50fd36e72a315b3", size = 275205, upload-time = "2026-01-23T15:30:24.556Z" }, + { url = "https://files.pythonhosted.org/packages/10/d3/be7d19e8fad7c5a78eeefb2d896a08cd4643e1e90c605c4be3b46264998f/greenlet-3.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65be2f026ca6a176f88fb935ee23c18333ccea97048076aef4db1ef5bc0713ac", size = 599284, upload-time = "2026-01-23T16:00:58.584Z" }, + { url = "https://files.pythonhosted.org/packages/ae/21/fe703aaa056fdb0f17e5afd4b5c80195bbdab701208918938bd15b00d39b/greenlet-3.3.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a3ae05b3d225b4155bda56b072ceb09d05e974bc74be6c3fc15463cf69f33fd", size = 610274, upload-time = "2026-01-23T16:05:29.312Z" }, + { url = "https://files.pythonhosted.org/packages/06/00/95df0b6a935103c0452dad2203f5be8377e551b8466a29650c4c5a5af6cc/greenlet-3.3.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:12184c61e5d64268a160226fb4818af4df02cfead8379d7f8b99a56c3a54ff3e", size = 624375, upload-time = "2026-01-23T16:15:55.915Z" }, + { url = "https://files.pythonhosted.org/packages/cb/86/5c6ab23bb3c28c21ed6bebad006515cfe08b04613eb105ca0041fecca852/greenlet-3.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6423481193bbbe871313de5fd06a082f2649e7ce6e08015d2a76c1e9186ca5b3", size = 612904, upload-time = "2026-01-23T15:32:52.317Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/7949994264e22639e40718c2daf6f6df5169bf48fb038c008a489ec53a50/greenlet-3.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:33a956fe78bbbda82bfc95e128d61129b32d66bcf0a20a1f0c08aa4839ffa951", size = 1567316, upload-time = "2026-01-23T16:04:23.316Z" }, + { url = "https://files.pythonhosted.org/packages/8d/6e/d73c94d13b6465e9f7cd6231c68abde838bb22408596c05d9059830b7872/greenlet-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b065d3284be43728dd280f6f9a13990b56470b81be20375a207cdc814a983f2", size = 1636549, upload-time = "2026-01-23T15:33:48.643Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b3/c9c23a6478b3bcc91f979ce4ca50879e4d0b2bd7b9a53d8ecded719b92e2/greenlet-3.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:27289986f4e5b0edec7b5a91063c109f0276abb09a7e9bdab08437525977c946", size = 227042, upload-time = "2026-01-23T15:33:58.216Z" }, + { url = "https://files.pythonhosted.org/packages/90/e7/824beda656097edee36ab15809fd063447b200cc03a7f6a24c34d520bc88/greenlet-3.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:2f080e028001c5273e0b42690eaf359aeef9cb1389da0f171ea51a5dc3c7608d", size = 226294, upload-time = "2026-01-23T15:30:52.73Z" }, + { url = "https://files.pythonhosted.org/packages/ae/fb/011c7c717213182caf78084a9bea51c8590b0afda98001f69d9f853a495b/greenlet-3.3.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:bd59acd8529b372775cd0fcbc5f420ae20681c5b045ce25bd453ed8455ab99b5", size = 275737, upload-time = "2026-01-23T15:32:16.889Z" }, + { url = "https://files.pythonhosted.org/packages/41/2e/a3a417d620363fdbb08a48b1dd582956a46a61bf8fd27ee8164f9dfe87c2/greenlet-3.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b31c05dd84ef6871dd47120386aed35323c944d86c3d91a17c4b8d23df62f15b", size = 646422, upload-time = "2026-01-23T16:01:00.354Z" }, + { url = "https://files.pythonhosted.org/packages/b4/09/c6c4a0db47defafd2d6bab8ddfe47ad19963b4e30f5bed84d75328059f8c/greenlet-3.3.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02925a0bfffc41e542c70aa14c7eda3593e4d7e274bfcccca1827e6c0875902e", size = 658219, upload-time = "2026-01-23T16:05:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/e2/89/b95f2ddcc5f3c2bc09c8ee8d77be312df7f9e7175703ab780f2014a0e781/greenlet-3.3.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3e0f3878ca3a3ff63ab4ea478585942b53df66ddde327b59ecb191b19dbbd62d", size = 671455, upload-time = "2026-01-23T16:15:57.232Z" }, + { url = "https://files.pythonhosted.org/packages/80/38/9d42d60dffb04b45f03dbab9430898352dba277758640751dc5cc316c521/greenlet-3.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34a729e2e4e4ffe9ae2408d5ecaf12f944853f40ad724929b7585bca808a9d6f", size = 660237, upload-time = "2026-01-23T15:32:53.967Z" }, + { url = "https://files.pythonhosted.org/packages/96/61/373c30b7197f9e756e4c81ae90a8d55dc3598c17673f91f4d31c3c689c3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aec9ab04e82918e623415947921dea15851b152b822661cce3f8e4393c3df683", size = 1615261, upload-time = "2026-01-23T16:04:25.066Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d3/ca534310343f5945316f9451e953dcd89b36fe7a19de652a1dc5a0eeef3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71c767cf281a80d02b6c1bdc41c9468e1f5a494fb11bc8688c360524e273d7b1", size = 1683719, upload-time = "2026-01-23T15:33:50.61Z" }, + { url = "https://files.pythonhosted.org/packages/52/cb/c21a3fd5d2c9c8b622e7bede6d6d00e00551a5ee474ea6d831b5f567a8b4/greenlet-3.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:96aff77af063b607f2489473484e39a0bbae730f2ea90c9e5606c9b73c44174a", size = 228125, upload-time = "2026-01-23T15:32:45.265Z" }, + { url = "https://files.pythonhosted.org/packages/6a/8e/8a2db6d11491837af1de64b8aff23707c6e85241be13c60ed399a72e2ef8/greenlet-3.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:b066e8b50e28b503f604fa538adc764a638b38cf8e81e025011d26e8a627fa79", size = 227519, upload-time = "2026-01-23T15:31:47.284Z" }, + { url = "https://files.pythonhosted.org/packages/28/24/cbbec49bacdcc9ec652a81d3efef7b59f326697e7edf6ed775a5e08e54c2/greenlet-3.3.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:3e63252943c921b90abb035ebe9de832c436401d9c45f262d80e2d06cc659242", size = 282706, upload-time = "2026-01-23T15:33:05.525Z" }, + { url = "https://files.pythonhosted.org/packages/86/2e/4f2b9323c144c4fe8842a4e0d92121465485c3c2c5b9e9b30a52e80f523f/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76e39058e68eb125de10c92524573924e827927df5d3891fbc97bd55764a8774", size = 651209, upload-time = "2026-01-23T16:01:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/d9/87/50ca60e515f5bb55a2fbc5f0c9b5b156de7d2fc51a0a69abc9d23914a237/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9f9d5e7a9310b7a2f416dd13d2e3fd8b42d803968ea580b7c0f322ccb389b97", size = 654300, upload-time = "2026-01-23T16:05:32.199Z" }, + { url = "https://files.pythonhosted.org/packages/7c/25/c51a63f3f463171e09cb586eb64db0861eb06667ab01a7968371a24c4f3b/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b9721549a95db96689458a1e0ae32412ca18776ed004463df3a9299c1b257ab", size = 662574, upload-time = "2026-01-23T16:15:58.364Z" }, + { url = "https://files.pythonhosted.org/packages/1d/94/74310866dfa2b73dd08659a3d18762f83985ad3281901ba0ee9a815194fb/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92497c78adf3ac703b57f1e3813c2d874f27f71a178f9ea5887855da413cd6d2", size = 653842, upload-time = "2026-01-23T15:32:55.671Z" }, + { url = "https://files.pythonhosted.org/packages/97/43/8bf0ffa3d498eeee4c58c212a3905dd6146c01c8dc0b0a046481ca29b18c/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ed6b402bc74d6557a705e197d47f9063733091ed6357b3de33619d8a8d93ac53", size = 1614917, upload-time = "2026-01-23T16:04:26.276Z" }, + { url = "https://files.pythonhosted.org/packages/89/90/a3be7a5f378fc6e84abe4dcfb2ba32b07786861172e502388b4c90000d1b/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:59913f1e5ada20fde795ba906916aea25d442abcc0593fba7e26c92b7ad76249", size = 1676092, upload-time = "2026-01-23T15:33:52.176Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2b/98c7f93e6db9977aaee07eb1e51ca63bd5f779b900d362791d3252e60558/greenlet-3.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:301860987846c24cb8964bdec0e31a96ad4a2a801b41b4ef40963c1b44f33451", size = 233181, upload-time = "2026-01-23T15:33:00.29Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "h5netcdf" +version = "1.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/03/92d6cc02c0055158167255980461155d6e17f1c4143c03f8bcc18d3e3f3a/h5netcdf-1.8.1.tar.gz", hash = "sha256:9b396a4cc346050fc1a4df8523bc1853681ec3544e0449027ae397cb953c7a16", size = 78679, upload-time = "2026-01-23T07:35:31.233Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/8b/88f16936a8e8070a83d36239555227ecd91728f9ef222c5382cda07e0fd6/h5netcdf-1.8.1-py3-none-any.whl", hash = "sha256:a76ed7cfc9b8a8908ea7057c4e57e27307acff1049b7f5ed52db6c2247636879", size = 62915, upload-time = "2026-01-23T07:35:30.195Z" }, +] + +[[package]] +name = "h5py" +version = "3.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/6a/0d79de0b025aa85dc8864de8e97659c94cf3d23148394a954dc5ca52f8c8/h5py-3.15.1.tar.gz", hash = "sha256:c86e3ed45c4473564de55aa83b6fc9e5ead86578773dfbd93047380042e26b69", size = 426236, upload-time = "2025-10-16T10:35:27.404Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/fd/8349b48b15b47768042cff06ad6e1c229f0a4bd89225bf6b6894fea27e6d/h5py-3.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5aaa330bcbf2830150c50897ea5dcbed30b5b6d56897289846ac5b9e529ec243", size = 3434135, upload-time = "2025-10-16T10:33:47.954Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b0/1c628e26a0b95858f54aba17e1599e7f6cd241727596cc2580b72cb0a9bf/h5py-3.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c970fb80001fffabb0109eaf95116c8e7c0d3ca2de854e0901e8a04c1f098509", size = 2870958, upload-time = "2025-10-16T10:33:50.907Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e3/c255cafc9b85e6ea04e2ad1bba1416baa1d7f57fc98a214be1144087690c/h5py-3.15.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80e5bb5b9508d5d9da09f81fd00abbb3f85da8143e56b1585d59bc8ceb1dba8b", size = 4504770, upload-time = "2025-10-16T10:33:54.357Z" }, + { url = "https://files.pythonhosted.org/packages/8b/23/4ab1108e87851ccc69694b03b817d92e142966a6c4abd99e17db77f2c066/h5py-3.15.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b849ba619a066196169763c33f9f0f02e381156d61c03e000bb0100f9950faf", size = 4700329, upload-time = "2025-10-16T10:33:57.616Z" }, + { url = "https://files.pythonhosted.org/packages/a4/e4/932a3a8516e4e475b90969bf250b1924dbe3612a02b897e426613aed68f4/h5py-3.15.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e7f6c841efd4e6e5b7e82222eaf90819927b6d256ab0f3aca29675601f654f3c", size = 4152456, upload-time = "2025-10-16T10:34:00.843Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0a/f74d589883b13737021b2049ac796328f188dbb60c2ed35b101f5b95a3fc/h5py-3.15.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ca8a3a22458956ee7b40d8e39c9a9dc01f82933e4c030c964f8b875592f4d831", size = 4617295, upload-time = "2025-10-16T10:34:04.154Z" }, + { url = "https://files.pythonhosted.org/packages/23/95/499b4e56452ef8b6c95a271af0dde08dac4ddb70515a75f346d4f400579b/h5py-3.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:550e51131376889656feec4aff2170efc054a7fe79eb1da3bb92e1625d1ac878", size = 2882129, upload-time = "2025-10-16T10:34:06.886Z" }, + { url = "https://files.pythonhosted.org/packages/ce/bb/cfcc70b8a42222ba3ad4478bcef1791181ea908e2adbd7d53c66395edad5/h5py-3.15.1-cp311-cp311-win_arm64.whl", hash = "sha256:b39239947cb36a819147fc19e86b618dcb0953d1cd969f5ed71fc0de60392427", size = 2477121, upload-time = "2025-10-16T10:34:09.579Z" }, + { url = "https://files.pythonhosted.org/packages/62/b8/c0d9aa013ecfa8b7057946c080c0c07f6fa41e231d2e9bd306a2f8110bdc/h5py-3.15.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:316dd0f119734f324ca7ed10b5627a2de4ea42cc4dfbcedbee026aaa361c238c", size = 3399089, upload-time = "2025-10-16T10:34:12.135Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5e/3c6f6e0430813c7aefe784d00c6711166f46225f5d229546eb53032c3707/h5py-3.15.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b51469890e58e85d5242e43aab29f5e9c7e526b951caab354f3ded4ac88e7b76", size = 2847803, upload-time = "2025-10-16T10:34:14.564Z" }, + { url = "https://files.pythonhosted.org/packages/00/69/ba36273b888a4a48d78f9268d2aee05787e4438557450a8442946ab8f3ec/h5py-3.15.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a33bfd5dfcea037196f7778534b1ff7e36a7f40a89e648c8f2967292eb6898e", size = 4914884, upload-time = "2025-10-16T10:34:18.452Z" }, + { url = "https://files.pythonhosted.org/packages/3a/30/d1c94066343a98bb2cea40120873193a4fed68c4ad7f8935c11caf74c681/h5py-3.15.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25c8843fec43b2cc368aa15afa1cdf83fc5e17b1c4e10cd3771ef6c39b72e5ce", size = 5109965, upload-time = "2025-10-16T10:34:21.853Z" }, + { url = "https://files.pythonhosted.org/packages/81/3d/d28172116eafc3bc9f5991b3cb3fd2c8a95f5984f50880adfdf991de9087/h5py-3.15.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a308fd8681a864c04423c0324527237a0484e2611e3441f8089fd00ed56a8171", size = 4561870, upload-time = "2025-10-16T10:34:26.69Z" }, + { url = "https://files.pythonhosted.org/packages/a5/83/393a7226024238b0f51965a7156004eaae1fcf84aa4bfecf7e582676271b/h5py-3.15.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f4a016df3f4a8a14d573b496e4d1964deb380e26031fc85fb40e417e9131888a", size = 5037161, upload-time = "2025-10-16T10:34:30.383Z" }, + { url = "https://files.pythonhosted.org/packages/cf/51/329e7436bf87ca6b0fe06dd0a3795c34bebe4ed8d6c44450a20565d57832/h5py-3.15.1-cp312-cp312-win_amd64.whl", hash = "sha256:59b25cf02411bf12e14f803fef0b80886444c7fe21a5ad17c6a28d3f08098a1e", size = 2874165, upload-time = "2025-10-16T10:34:33.461Z" }, + { url = "https://files.pythonhosted.org/packages/09/a8/2d02b10a66747c54446e932171dd89b8b4126c0111b440e6bc05a7c852ec/h5py-3.15.1-cp312-cp312-win_arm64.whl", hash = "sha256:61d5a58a9851e01ee61c932bbbb1c98fe20aba0a5674776600fb9a361c0aa652", size = 2458214, upload-time = "2025-10-16T10:34:35.733Z" }, + { url = "https://files.pythonhosted.org/packages/88/b3/40207e0192415cbff7ea1d37b9f24b33f6d38a5a2f5d18a678de78f967ae/h5py-3.15.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c8440fd8bee9500c235ecb7aa1917a0389a2adb80c209fa1cc485bd70e0d94a5", size = 3376511, upload-time = "2025-10-16T10:34:38.596Z" }, + { url = "https://files.pythonhosted.org/packages/31/96/ba99a003c763998035b0de4c299598125df5fc6c9ccf834f152ddd60e0fb/h5py-3.15.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ab2219dbc6fcdb6932f76b548e2b16f34a1f52b7666e998157a4dfc02e2c4123", size = 2826143, upload-time = "2025-10-16T10:34:41.342Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c2/fc6375d07ea3962df7afad7d863fe4bde18bb88530678c20d4c90c18de1d/h5py-3.15.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8cb02c3a96255149ed3ac811eeea25b655d959c6dd5ce702c9a95ff11859eb5", size = 4908316, upload-time = "2025-10-16T10:34:44.619Z" }, + { url = "https://files.pythonhosted.org/packages/d9/69/4402ea66272dacc10b298cca18ed73e1c0791ff2ae9ed218d3859f9698ac/h5py-3.15.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:121b2b7a4c1915d63737483b7bff14ef253020f617c2fb2811f67a4bed9ac5e8", size = 5103710, upload-time = "2025-10-16T10:34:48.639Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f6/11f1e2432d57d71322c02a97a5567829a75f223a8c821764a0e71a65cde8/h5py-3.15.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59b0d63b318bf3cc06687def2b45afd75926bbc006f7b8cd2b1a231299fc8599", size = 4556042, upload-time = "2025-10-16T10:34:51.841Z" }, + { url = "https://files.pythonhosted.org/packages/18/88/3eda3ef16bfe7a7dbc3d8d6836bbaa7986feb5ff091395e140dc13927bcc/h5py-3.15.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e02fe77a03f652500d8bff288cbf3675f742fc0411f5a628fa37116507dc7cc0", size = 5030639, upload-time = "2025-10-16T10:34:55.257Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ea/fbb258a98863f99befb10ed727152b4ae659f322e1d9c0576f8a62754e81/h5py-3.15.1-cp313-cp313-win_amd64.whl", hash = "sha256:dea78b092fd80a083563ed79a3171258d4a4d307492e7cf8b2313d464c82ba52", size = 2864363, upload-time = "2025-10-16T10:34:58.099Z" }, + { url = "https://files.pythonhosted.org/packages/5d/c9/35021cc9cd2b2915a7da3026e3d77a05bed1144a414ff840953b33937fb9/h5py-3.15.1-cp313-cp313-win_arm64.whl", hash = "sha256:c256254a8a81e2bddc0d376e23e2a6d2dc8a1e8a2261835ed8c1281a0744cd97", size = 2449570, upload-time = "2025-10-16T10:35:00.473Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2c/926eba1514e4d2e47d0e9eb16c784e717d8b066398ccfca9b283917b1bfb/h5py-3.15.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5f4fb0567eb8517c3ecd6b3c02c4f4e9da220c8932604960fd04e24ee1254763", size = 3380368, upload-time = "2025-10-16T10:35:03.117Z" }, + { url = "https://files.pythonhosted.org/packages/65/4b/d715ed454d3baa5f6ae1d30b7eca4c7a1c1084f6a2edead9e801a1541d62/h5py-3.15.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:954e480433e82d3872503104f9b285d369048c3a788b2b1a00e53d1c47c98dd2", size = 2833793, upload-time = "2025-10-16T10:35:05.623Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d4/ef386c28e4579314610a8bffebbee3b69295b0237bc967340b7c653c6c10/h5py-3.15.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd125c131889ebbef0849f4a0e29cf363b48aba42f228d08b4079913b576bb3a", size = 4903199, upload-time = "2025-10-16T10:35:08.972Z" }, + { url = "https://files.pythonhosted.org/packages/33/5d/65c619e195e0b5e54ea5a95c1bb600c8ff8715e0d09676e4cce56d89f492/h5py-3.15.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28a20e1a4082a479b3d7db2169f3a5034af010b90842e75ebbf2e9e49eb4183e", size = 5097224, upload-time = "2025-10-16T10:35:12.808Z" }, + { url = "https://files.pythonhosted.org/packages/30/30/5273218400bf2da01609e1292f562c94b461fcb73c7a9e27fdadd43abc0a/h5py-3.15.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa8df5267f545b4946df8ca0d93d23382191018e4cda2deda4c2cedf9a010e13", size = 4551207, upload-time = "2025-10-16T10:35:16.24Z" }, + { url = "https://files.pythonhosted.org/packages/d3/39/a7ef948ddf4d1c556b0b2b9559534777bccc318543b3f5a1efdf6b556c9c/h5py-3.15.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99d374a21f7321a4c6ab327c4ab23bd925ad69821aeb53a1e75dd809d19f67fa", size = 5025426, upload-time = "2025-10-16T10:35:19.831Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d8/7368679b8df6925b8415f9dcc9ab1dab01ddc384d2b2c24aac9191bd9ceb/h5py-3.15.1-cp314-cp314-win_amd64.whl", hash = "sha256:9c73d1d7cdb97d5b17ae385153472ce118bed607e43be11e9a9deefaa54e0734", size = 2865704, upload-time = "2025-10-16T10:35:22.658Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b7/4a806f85d62c20157e62e58e03b27513dc9c55499768530acc4f4c5ce4be/h5py-3.15.1-cp314-cp314-win_arm64.whl", hash = "sha256:a6d8c5a05a76aca9a494b4c53ce8a9c29023b7f64f625c6ce1841e92a362ccdf", size = 2465544, upload-time = "2025-10-16T10:35:25.695Z" }, +] + +[[package]] +name = "holoviews" +version = "1.22.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bokeh" }, + { name = "colorcet" }, + { name = "narwhals" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "panel" }, + { name = "param" }, + { name = "python-dateutil" }, + { name = "pyviz-comms" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/e4/40c001bae370f9e89bbcda71c6fb75bd5baa4ec8d5004369a591cfaa0d5d/holoviews-1.22.1.tar.gz", hash = "sha256:7dde3a10bb77aff0982e21786b4f5249c6a9d70c463f5604a49fcf30c0247756", size = 5509590, upload-time = "2025-12-05T14:54:20.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/ab/a6aa43d45ceb88adc0e8c1358fa6935c6e6a5895537431dec67524ca2ccd/holoviews-1.22.1-py3-none-any.whl", hash = "sha256:6f4f0656336035cde1d8103ac6461d7c8ac9a60c4a6d883785cc81f2cc5b8702", size = 5946246, upload-time = "2025-12-05T14:54:19.191Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "hvplot" +version = "0.12.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bokeh" }, + { name = "colorcet" }, + { name = "holoviews" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "panel" }, + { name = "param" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/b4/ce73a568fc263efe84cab8de04673949c8f6596f6aeb1531ca2b75b80903/hvplot-0.12.2.tar.gz", hash = "sha256:9e29bbe65f94937d4eeaeccf33566540df936c8f9aeadfa12ea9f306d5237938", size = 7084556, upload-time = "2025-12-18T11:15:37.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/cd/ec193e471780dfad60e44ded5526c123695c1354910edd537d7aa1d22094/hvplot-0.12.2-py3-none-any.whl", hash = "sha256:0687e2e4d2eeb035c437af0011922abff856054299c121914d903a02b1bb1b22", size = 180626, upload-time = "2025-12-18T11:15:35.091Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "ipykernel" +version = "7.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appnope", marker = "sys_platform == 'darwin'" }, + { name = "comm" }, + { name = "debugpy" }, + { name = "ipython" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "matplotlib-inline" }, + { name = "nest-asyncio" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/8d/b68b728e2d06b9e0051019640a40a9eb7a88fcd82c2e1b5ce70bef5ff044/ipykernel-7.2.0.tar.gz", hash = "sha256:18ed160b6dee2cbb16e5f3575858bc19d8f1fe6046a9a680c708494ce31d909e", size = 176046, upload-time = "2026-02-06T16:43:27.403Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl", hash = "sha256:3bbd4420d2b3cc105cbdf3756bfc04500b1e52f090a90716851f3916c62e1661", size = 118788, upload-time = "2026-02-06T16:43:25.149Z" }, +] + +[[package]] +name = "ipython" +version = "9.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "ipython-pygments-lexers" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/60/2111715ea11f39b1535bed6024b7dec7918b71e5e5d30855a5b503056b50/ipython-9.10.0.tar.gz", hash = "sha256:cd9e656be97618a0676d058134cd44e6dc7012c0e5cb36a9ce96a8c904adaf77", size = 4426526, upload-time = "2026-02-02T10:00:33.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/aa/898dec789a05731cd5a9f50605b7b44a72bd198fd0d4528e11fc610177cc/ipython-9.10.0-py3-none-any.whl", hash = "sha256:c6ab68cc23bba8c7e18e9b932797014cc61ea7fd6f19de180ab9ba73e65ee58d", size = 622774, upload-time = "2026-02-02T10:00:31.503Z" }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, +] + +[[package]] +name = "ipywidgets" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "comm" }, + { name = "ipython" }, + { name = "jupyterlab-widgets" }, + { name = "traitlets" }, + { name = "widgetsnbextension" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/ae/c5ce1edc1afe042eadb445e95b0671b03cee61895264357956e61c0d2ac0/ipywidgets-8.1.8.tar.gz", hash = "sha256:61f969306b95f85fba6b6986b7fe45d73124d1d9e3023a8068710d47a22ea668", size = 116739, upload-time = "2025-11-01T21:18:12.393Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl", hash = "sha256:ecaca67aed704a338f88f67b1181b58f821ab5dc89c1f0f5ef99db43c1c2921e", size = 139808, upload-time = "2025-11-01T21:18:10.956Z" }, +] + +[[package]] +name = "isoduration" +version = "20.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "arrow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/1a/3c8edc664e06e6bd06cce40c6b22da5f1429aa4224d0c590f3be21c91ead/isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9", size = 11649, upload-time = "2020-11-01T11:00:00.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321, upload-time = "2020-11-01T10:59:58.02Z" }, +] + +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "json5" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/77/e8/a3f261a66e4663f22700bc8a17c08cb83e91fbf086726e7a228398968981/json5-0.13.0.tar.gz", hash = "sha256:b1edf8d487721c0bf64d83c28e91280781f6e21f4a797d3261c7c828d4c165bf", size = 52441, upload-time = "2026-01-01T19:42:14.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/9e/038522f50ceb7e74f1f991bf1b699f24b0c2bbe7c390dd36ad69f4582258/json5-0.13.0-py3-none-any.whl", hash = "sha256:9a08e1dd65f6a4d4c6fa82d216cf2477349ec2346a38fd70cc11d2557499fbcc", size = 36163, upload-time = "2026-01-01T19:42:13.962Z" }, +] + +[[package]] +name = "jsonpointer" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[package.optional-dependencies] +format-nongpl = [ + { name = "fqdn" }, + { name = "idna" }, + { name = "isoduration" }, + { name = "jsonpointer" }, + { name = "rfc3339-validator" }, + { name = "rfc3986-validator" }, + { name = "rfc3987-syntax" }, + { name = "uri-template" }, + { name = "webcolors" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "jupyter-bokeh" +version = "4.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bokeh" }, + { name = "ipywidgets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/fd/8f0213c704bf36b5f523ae5bf7dc367f3687e75dcc2354084b75c05d2b53/jupyter_bokeh-4.0.5.tar.gz", hash = "sha256:a33d6ab85588f13640b30765fa15d1111b055cbe44f67a65ca57d3593af8245d", size = 149140, upload-time = "2024-06-03T06:33:33.488Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/78/33b2294aad62e5f95b89a89379c5995c2bd978018387ef8bec79f6dc272c/jupyter_bokeh-4.0.5-py3-none-any.whl", hash = "sha256:1110076c14c779071cf492646a1a871aefa8a477261e4721327a666e65df1a2c", size = 148593, upload-time = "2024-06-03T06:33:35.82Z" }, +] + +[[package]] +name = "jupyter-cache" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "click" }, + { name = "importlib-metadata" }, + { name = "nbclient" }, + { name = "nbformat" }, + { name = "pyyaml" }, + { name = "sqlalchemy" }, + { name = "tabulate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/f7/3627358075f183956e8c4974603232b03afd4ddc7baf72c2bc9fff522291/jupyter_cache-1.0.1.tar.gz", hash = "sha256:16e808eb19e3fb67a223db906e131ea6e01f03aa27f49a7214ce6a5fec186fb9", size = 32048, upload-time = "2024-11-15T16:03:55.322Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/6b/67b87da9d36bff9df7d0efbd1a325fa372a43be7158effaf43ed7b22341d/jupyter_cache-1.0.1-py3-none-any.whl", hash = "sha256:9c3cafd825ba7da8b5830485343091143dff903e4d8c69db9349b728b140abf6", size = 33907, upload-time = "2024-11-15T16:03:54.021Z" }, +] + +[[package]] +name = "jupyter-client" +version = "8.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-core" }, + { name = "python-dateutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/e4/ba649102a3bc3fbca54e7239fb924fd434c766f855693d86de0b1f2bec81/jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e", size = 348020, upload-time = "2026-01-08T13:55:47.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl", hash = "sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a", size = 107371, upload-time = "2026-01-08T13:55:45.562Z" }, +] + +[[package]] +name = "jupyter-core" +version = "5.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, +] + +[[package]] +name = "jupyter-events" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema", extra = ["format-nongpl"] }, + { name = "packaging" }, + { name = "python-json-logger" }, + { name = "pyyaml" }, + { name = "referencing" }, + { name = "rfc3339-validator" }, + { name = "rfc3986-validator" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/c3/306d090461e4cf3cd91eceaff84bede12a8e52cd821c2d20c9a4fd728385/jupyter_events-0.12.0.tar.gz", hash = "sha256:fc3fce98865f6784c9cd0a56a20644fc6098f21c8c33834a8d9fe383c17e554b", size = 62196, upload-time = "2025-02-03T17:23:41.485Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl", hash = "sha256:6464b2fa5ad10451c3d35fabc75eab39556ae1e2853ad0c0cc31b656731a97fb", size = 19430, upload-time = "2025-02-03T17:23:38.643Z" }, +] + +[[package]] +name = "jupyter-lsp" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/5a/9066c9f8e94ee517133cd98dba393459a16cd48bba71a82f16a65415206c/jupyter_lsp-2.3.0.tar.gz", hash = "sha256:458aa59339dc868fb784d73364f17dbce8836e906cd75fd471a325cba02e0245", size = 54823, upload-time = "2025-08-27T17:47:34.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl", hash = "sha256:e914a3cb2addf48b1c7710914771aaf1819d46b2e5a79b0f917b5478ec93f34f", size = 76687, upload-time = "2025-08-27T17:47:33.15Z" }, +] + +[[package]] +name = "jupyter-server" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "argon2-cffi" }, + { name = "jinja2" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "jupyter-events" }, + { name = "jupyter-server-terminals" }, + { name = "nbconvert" }, + { name = "nbformat" }, + { name = "overrides", marker = "python_full_version < '3.12'" }, + { name = "packaging" }, + { name = "prometheus-client" }, + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "pyzmq" }, + { name = "send2trash" }, + { name = "terminado" }, + { name = "tornado" }, + { name = "traitlets" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/ac/e040ec363d7b6b1f11304cc9f209dac4517ece5d5e01821366b924a64a50/jupyter_server-2.17.0.tar.gz", hash = "sha256:c38ea898566964c888b4772ae1ed58eca84592e88251d2cfc4d171f81f7e99d5", size = 731949, upload-time = "2025-08-21T14:42:54.042Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl", hash = "sha256:e8cb9c7db4251f51ed307e329b81b72ccf2056ff82d50524debde1ee1870e13f", size = 388221, upload-time = "2025-08-21T14:42:52.034Z" }, +] + +[[package]] +name = "jupyter-server-terminals" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "terminado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/a7/bcd0a9b0cbba88986fe944aaaf91bfda603e5a50bda8ed15123f381a3b2f/jupyter_server_terminals-0.5.4.tar.gz", hash = "sha256:bbda128ed41d0be9020349f9f1f2a4ab9952a73ed5f5ac9f1419794761fb87f5", size = 31770, upload-time = "2026-01-14T16:53:20.213Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl", hash = "sha256:55be353fc74a80bc7f3b20e6be50a55a61cd525626f578dcb66a5708e2007d14", size = 13704, upload-time = "2026-01-14T16:53:18.738Z" }, +] + +[[package]] +name = "jupyterlab" +version = "4.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-lru" }, + { name = "httpx" }, + { name = "ipykernel" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyter-lsp" }, + { name = "jupyter-server" }, + { name = "jupyterlab-server" }, + { name = "notebook-shim" }, + { name = "packaging" }, + { name = "setuptools" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/76/393eae3349f9a39bf21f8f5406e5244d36e2bfc932049b6070c271f92764/jupyterlab-4.5.3.tar.gz", hash = "sha256:4a159f71067cb38e4a82e86a42de8e7e926f384d7f2291964f282282096d27e8", size = 23939231, upload-time = "2026-01-23T15:04:25.768Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/9a/0bf9a7a45f0006d7ff4fdc4fc313de4255acab02bf4db1887c65f0472c01/jupyterlab-4.5.3-py3-none-any.whl", hash = "sha256:63c9f3a48de72ba00df766ad6eed416394f5bb883829f11eeff0872302520ba7", size = 12391761, upload-time = "2026-01-23T15:04:21.214Z" }, +] + +[[package]] +name = "jupyterlab-pygments" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900, upload-time = "2023-11-23T09:26:37.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" }, +] + +[[package]] +name = "jupyterlab-server" +version = "2.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "jinja2" }, + { name = "json5" }, + { name = "jsonschema" }, + { name = "jupyter-server" }, + { name = "packaging" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/2c/90153f189e421e93c4bb4f9e3f59802a1f01abd2ac5cf40b152d7f735232/jupyterlab_server-2.28.0.tar.gz", hash = "sha256:35baa81898b15f93573e2deca50d11ac0ae407ebb688299d3a5213265033712c", size = 76996, upload-time = "2025-10-22T13:59:18.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl", hash = "sha256:e4355b148fdcf34d312bbbc80f22467d6d20460e8b8736bf235577dd18506968", size = 59830, upload-time = "2025-10-22T13:59:16.767Z" }, +] + +[[package]] +name = "jupyterlab-widgets" +version = "3.0.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/2d/ef58fed122b268c69c0aa099da20bc67657cdfb2e222688d5731bd5b971d/jupyterlab_widgets-3.0.16.tar.gz", hash = "sha256:423da05071d55cf27a9e602216d35a3a65a3e41cdf9c5d3b643b814ce38c19e0", size = 897423, upload-time = "2025-11-01T21:11:29.724Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl", hash = "sha256:45fa36d9c6422cf2559198e4db481aa243c7a32d9926b500781c830c80f7ecf8", size = 914926, upload-time = "2025-11-01T21:11:28.008Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" }, + { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" }, + { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" }, + { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" }, + { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" }, + { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, + { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" }, + { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" }, + { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" }, + { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" }, + { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, + { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, + { url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" }, + { url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607, upload-time = "2025-08-10T21:26:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546, upload-time = "2025-08-10T21:26:31.401Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482, upload-time = "2025-08-10T21:26:32.721Z" }, + { url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720, upload-time = "2025-08-10T21:26:34.032Z" }, + { url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907, upload-time = "2025-08-10T21:26:35.824Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" }, + { url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" }, + { url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" }, + { url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135, upload-time = "2025-08-10T21:26:48.665Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409, upload-time = "2025-08-10T21:26:50.335Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763, upload-time = "2025-08-10T21:26:51.867Z" }, + { url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643, upload-time = "2025-08-10T21:26:53.592Z" }, + { url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818, upload-time = "2025-08-10T21:26:55.051Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" }, + { url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" }, + { url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806, upload-time = "2025-08-10T21:27:01.537Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605, upload-time = "2025-08-10T21:27:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925, upload-time = "2025-08-10T21:27:04.339Z" }, + { url = "https://files.pythonhosted.org/packages/43/dc/51acc6791aa14e5cb6d8a2e28cefb0dc2886d8862795449d021334c0df20/kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58", size = 1472414, upload-time = "2025-08-10T21:27:05.437Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/93fa64a81db304ac8a246f834d5094fae4b13baf53c839d6bb6e81177129/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4", size = 1281272, upload-time = "2025-08-10T21:27:07.063Z" }, + { url = "https://files.pythonhosted.org/packages/70/e6/6df102916960fb8d05069d4bd92d6d9a8202d5a3e2444494e7cd50f65b7a/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df", size = 1298578, upload-time = "2025-08-10T21:27:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/7c/47/e142aaa612f5343736b087864dbaebc53ea8831453fb47e7521fa8658f30/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6", size = 1345607, upload-time = "2025-08-10T21:27:10.125Z" }, + { url = "https://files.pythonhosted.org/packages/54/89/d641a746194a0f4d1a3670fb900d0dbaa786fb98341056814bc3f058fa52/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5", size = 2230150, upload-time = "2025-08-10T21:27:11.484Z" }, + { url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979, upload-time = "2025-08-10T21:27:12.917Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456, upload-time = "2025-08-10T21:27:14.353Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621, upload-time = "2025-08-10T21:27:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417, upload-time = "2025-08-10T21:27:17.436Z" }, + { url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582, upload-time = "2025-08-10T21:27:18.436Z" }, + { url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514, upload-time = "2025-08-10T21:27:19.465Z" }, + { url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905, upload-time = "2025-08-10T21:27:20.51Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399, upload-time = "2025-08-10T21:27:21.496Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ae/d7ba902aa604152c2ceba5d352d7b62106bedbccc8e95c3934d94472bfa3/kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122", size = 1582197, upload-time = "2025-08-10T21:27:22.604Z" }, + { url = "https://files.pythonhosted.org/packages/f2/41/27c70d427eddb8bc7e4f16420a20fefc6f480312122a59a959fdfe0445ad/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64", size = 1390125, upload-time = "2025-08-10T21:27:24.036Z" }, + { url = "https://files.pythonhosted.org/packages/41/42/b3799a12bafc76d962ad69083f8b43b12bf4fe78b097b12e105d75c9b8f1/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134", size = 1402612, upload-time = "2025-08-10T21:27:25.773Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b5/a210ea073ea1cfaca1bb5c55a62307d8252f531beb364e18aa1e0888b5a0/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370", size = 1453990, upload-time = "2025-08-10T21:27:27.089Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ce/a829eb8c033e977d7ea03ed32fb3c1781b4fa0433fbadfff29e39c676f32/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21", size = 2331601, upload-time = "2025-08-10T21:27:29.343Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041, upload-time = "2025-08-10T21:27:30.754Z" }, + { url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897, upload-time = "2025-08-10T21:27:32.803Z" }, + { url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" }, + { url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" }, + { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" }, + { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, +] + +[[package]] +name = "labcore" +version = "1.0.0" +source = { editable = "." } +dependencies = [ + { name = "bokeh" }, + { name = "gitpython" }, + { name = "h5py" }, + { name = "holoviews" }, + { name = "hvplot" }, + { name = "jupyter-bokeh" }, + { name = "jupyterlab" }, + { name = "lmfit" }, + { name = "matplotlib" }, + { name = "nest-asyncio" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "panel" }, + { name = "param" }, + { name = "pillow" }, + { name = "qcodes" }, + { name = "ruamel-yaml" }, + { name = "seaborn" }, + { name = "watchdog" }, + { name = "xarray" }, +] + +[package.dev-dependencies] +dev = [ + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, + { name = "types-markdown" }, +] +docs = [ + { name = "h5py" }, + { name = "ipykernel" }, + { name = "linkify-it-py" }, + { name = "myst-nb" }, + { name = "myst-parser" }, + { name = "pandoc" }, + { name = "pydata-sphinx-theme" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] + +[package.metadata] +requires-dist = [ + { name = "bokeh" }, + { name = "gitpython" }, + { name = "h5py" }, + { name = "holoviews" }, + { name = "hvplot" }, + { name = "jupyter-bokeh" }, + { name = "jupyterlab" }, + { name = "lmfit" }, + { name = "matplotlib" }, + { name = "nest-asyncio" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "panel" }, + { name = "param" }, + { name = "pillow" }, + { name = "qcodes" }, + { name = "ruamel-yaml" }, + { name = "seaborn" }, + { name = "watchdog" }, + { name = "xarray" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, + { name = "types-markdown", specifier = ">=3.10.2.20260211" }, +] +docs = [ + { name = "h5py" }, + { name = "ipykernel" }, + { name = "linkify-it-py" }, + { name = "myst-nb" }, + { name = "myst-parser" }, + { name = "pandoc" }, + { name = "pydata-sphinx-theme" }, + { name = "sphinx" }, +] + +[[package]] +name = "lark" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/34/28fff3ab31ccff1fd4f6c7c7b0ceb2b6968d8ea4950663eadcb5720591a0/lark-1.3.1.tar.gz", hash = "sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905", size = 382732, upload-time = "2025-10-27T18:25:56.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" }, +] + +[[package]] +name = "librt" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/01/0e748af5e4fee180cf7cd12bd12b0513ad23b045dccb2a83191bde82d168/librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd", size = 65315, upload-time = "2026-02-17T16:11:25.152Z" }, + { url = "https://files.pythonhosted.org/packages/9d/4d/7184806efda571887c798d573ca4134c80ac8642dcdd32f12c31b939c595/librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965", size = 68021, upload-time = "2026-02-17T16:11:26.129Z" }, + { url = "https://files.pythonhosted.org/packages/ae/88/c3c52d2a5d5101f28d3dc89298444626e7874aa904eed498464c2af17627/librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da", size = 194500, upload-time = "2026-02-17T16:11:27.177Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5d/6fb0a25b6a8906e85b2c3b87bee1d6ed31510be7605b06772f9374ca5cb3/librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0", size = 205622, upload-time = "2026-02-17T16:11:28.242Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a6/8006ae81227105476a45691f5831499e4d936b1c049b0c1feb17c11b02d1/librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e", size = 218304, upload-time = "2026-02-17T16:11:29.344Z" }, + { url = "https://files.pythonhosted.org/packages/ee/19/60e07886ad16670aae57ef44dada41912c90906a6fe9f2b9abac21374748/librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3", size = 211493, upload-time = "2026-02-17T16:11:30.445Z" }, + { url = "https://files.pythonhosted.org/packages/9c/cf/f666c89d0e861d05600438213feeb818c7514d3315bae3648b1fc145d2b6/librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac", size = 219129, upload-time = "2026-02-17T16:11:32.021Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ef/f1bea01e40b4a879364c031476c82a0dc69ce068daad67ab96302fed2d45/librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596", size = 213113, upload-time = "2026-02-17T16:11:33.192Z" }, + { url = "https://files.pythonhosted.org/packages/9b/80/cdab544370cc6bc1b72ea369525f547a59e6938ef6863a11ab3cd24759af/librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99", size = 212269, upload-time = "2026-02-17T16:11:34.373Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9c/48d6ed8dac595654f15eceab2035131c136d1ae9a1e3548e777bb6dbb95d/librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe", size = 234673, upload-time = "2026-02-17T16:11:36.063Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/35b68b1db517f27a01be4467593292eb5315def8900afad29fabf56304ba/librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb", size = 54597, upload-time = "2026-02-17T16:11:37.544Z" }, + { url = "https://files.pythonhosted.org/packages/71/02/796fe8f02822235966693f257bf2c79f40e11337337a657a8cfebba5febc/librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b", size = 61733, upload-time = "2026-02-17T16:11:38.691Z" }, + { url = "https://files.pythonhosted.org/packages/28/ad/232e13d61f879a42a4e7117d65e4984bb28371a34bb6fb9ca54ec2c8f54e/librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9", size = 52273, upload-time = "2026-02-17T16:11:40.308Z" }, + { url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" }, + { url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" }, + { url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" }, + { url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" }, + { url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" }, + { url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" }, + { url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" }, + { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" }, + { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" }, + { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" }, + { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" }, + { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" }, + { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" }, + { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" }, + { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" }, + { url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" }, + { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" }, + { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" }, + { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" }, + { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" }, + { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" }, + { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" }, + { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" }, + { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" }, + { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, +] + +[[package]] +name = "linkify-it-py" +version = "2.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "uc-micro-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/ae/bb56c6828e4797ba5a4821eec7c43b8bf40f69cda4d4f5f8c8a2810ec96a/linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048", size = 27946, upload-time = "2024-02-04T14:48:04.179Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/1e/b832de447dee8b582cac175871d2f6c3d5077cc56d5575cadba1fd1cccfa/linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79", size = 19820, upload-time = "2024-02-04T14:48:02.496Z" }, +] + +[[package]] +name = "lmfit" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asteval" }, + { name = "dill" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "uncertainties" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/e5/a35942aed2de95e228728c34609b51fe3ec9182398eac50d288eef313aa2/lmfit-1.3.4.tar.gz", hash = "sha256:3c22c28c43f717f6c5b4a3bd81e893a2149739c26a592c046f2e33c23cfbe497", size = 630720, upload-time = "2025-07-19T20:09:01.876Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl", hash = "sha256:afce1593b42324d37ae2908249b0c55445e2f4c1a0474ff706a8e2f7b5d949fa", size = 97662, upload-time = "2025-07-19T20:09:00.32Z" }, +] + +[[package]] +name = "locket" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/83/97b29fe05cb6ae28d2dbd30b81e2e402a3eed5f460c26e9eaa5895ceacf5/locket-1.0.0.tar.gz", hash = "sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632", size = 4350, upload-time = "2022-04-20T22:04:44.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl", hash = "sha256:b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3", size = 4398, upload-time = "2022-04-20T22:04:42.23Z" }, +] + +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215, upload-time = "2025-12-10T22:55:16.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625, upload-time = "2025-12-10T22:55:17.712Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614, upload-time = "2025-12-10T22:55:20.8Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997, upload-time = "2025-12-10T22:55:23.258Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825, upload-time = "2025-12-10T22:55:25.217Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090, upload-time = "2025-12-10T22:55:27.162Z" }, + { url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377, upload-time = "2025-12-10T22:55:29.185Z" }, + { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" }, + { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" }, + { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076, upload-time = "2025-12-10T22:55:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794, upload-time = "2025-12-10T22:55:46.252Z" }, + { url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474, upload-time = "2025-12-10T22:55:47.864Z" }, + { url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637, upload-time = "2025-12-10T22:55:50.048Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678, upload-time = "2025-12-10T22:55:52.21Z" }, + { url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686, upload-time = "2025-12-10T22:55:54.253Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917, upload-time = "2025-12-10T22:55:56.268Z" }, + { url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679, upload-time = "2025-12-10T22:55:57.856Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336, upload-time = "2025-12-10T22:55:59.371Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653, upload-time = "2025-12-10T22:56:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356, upload-time = "2025-12-10T22:56:02.95Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" }, + { url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" }, + { url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" }, + { url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" }, + { url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" }, + { url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198, upload-time = "2025-12-10T22:56:45.584Z" }, + { url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817, upload-time = "2025-12-10T22:56:47.339Z" }, + { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mistune" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/55/d01f0c4b45ade6536c51170b9043db8b2ec6ddf4a35c7ea3f5f559ac935b/mistune-3.2.0.tar.gz", hash = "sha256:708487c8a8cdd99c9d90eb3ed4c3ed961246ff78ac82f03418f5183ab70e398a", size = 95467, upload-time = "2025-12-23T11:36:34.994Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1", size = 53598, upload-time = "2025-12-23T11:36:33.211Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "myst-nb" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "ipykernel" }, + { name = "ipython" }, + { name = "jupyter-cache" }, + { name = "myst-parser" }, + { name = "nbclient" }, + { name = "nbformat" }, + { name = "pyyaml" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/83/a894bd8dea7a6e9f053502ee8413484dcbf75a219013d6a72e971c0fecfd/myst_nb-1.3.0.tar.gz", hash = "sha256:df3cd4680f51a5af673fd46b38b562be3559aef1475e906ed0f2e66e4587ce4b", size = 81963, upload-time = "2025-07-13T22:49:38.493Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/a6/03d410c114b8c4856579b3d294dafc27626a7690a552625eec42b16dfa41/myst_nb-1.3.0-py3-none-any.whl", hash = "sha256:1f36af3c19964960ec4e51ac30949b6ed6df220356ffa8d60dd410885e132d7d", size = 82396, upload-time = "2025-07-13T22:49:37.019Z" }, +] + +[[package]] +name = "myst-parser" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "jinja2" }, + { name = "markdown-it-py" }, + { name = "mdit-py-plugins" }, + { name = "pyyaml" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/fa/7b45eef11b7971f0beb29d27b7bfe0d747d063aa29e170d9edd004733c8a/myst_parser-5.0.0.tar.gz", hash = "sha256:f6f231452c56e8baa662cc352c548158f6a16fcbd6e3800fc594978002b94f3a", size = 98535, upload-time = "2026-01-15T09:08:18.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/ac/686789b9145413f1a61878c407210e41bfdb097976864e0913078b24098c/myst_parser-5.0.0-py3-none-any.whl", hash = "sha256:ab31e516024918296e169139072b81592336f2fef55b8986aa31c9f04b5f7211", size = 84533, upload-time = "2026-01-15T09:08:16.788Z" }, +] + +[[package]] +name = "narwhals" +version = "2.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/6f/713be67779028d482c6e0f2dde5bc430021b2578a4808c1c9f6d7ad48257/narwhals-2.16.0.tar.gz", hash = "sha256:155bb45132b370941ba0396d123cf9ed192bf25f39c4cea726f2da422ca4e145", size = 618268, upload-time = "2026-02-02T10:31:00.545Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/cc/7cb74758e6df95e0c4e1253f203b6dd7f348bf2f29cf89e9210a2416d535/narwhals-2.16.0-py3-none-any.whl", hash = "sha256:846f1fd7093ac69d63526e50732033e86c30ea0026a44d9b23991010c7d1485d", size = 443951, upload-time = "2026-02-02T10:30:58.635Z" }, +] + +[[package]] +name = "nbclient" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "nbformat" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/91/1c1d5a4b9a9ebba2b4e32b8c852c2975c872aec1fe42ab5e516b2cecd193/nbclient-0.10.4.tar.gz", hash = "sha256:1e54091b16e6da39e297b0ece3e10f6f29f4ac4e8ee515d29f8a7099bd6553c9", size = 62554, upload-time = "2025-12-23T07:45:46.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl", hash = "sha256:9162df5a7373d70d606527300a95a975a47c137776cd942e52d9c7e29ff83440", size = 25465, upload-time = "2025-12-23T07:45:44.51Z" }, +] + +[[package]] +name = "nbconvert" +version = "7.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "bleach", extra = ["css"] }, + { name = "defusedxml" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyterlab-pygments" }, + { name = "markupsafe" }, + { name = "mistune" }, + { name = "nbclient" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "pandocfilters" }, + { name = "pygments" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/47/81f886b699450d0569f7bc551df2b1673d18df7ff25cc0c21ca36ed8a5ff/nbconvert-7.17.0.tar.gz", hash = "sha256:1b2696f1b5be12309f6c7d707c24af604b87dfaf6d950794c7b07acab96dda78", size = 862855, upload-time = "2026-01-29T16:37:48.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/4b/8d5f796a792f8a25f6925a96032f098789f448571eb92011df1ae59e8ea8/nbconvert-7.17.0-py3-none-any.whl", hash = "sha256:4f99a63b337b9a23504347afdab24a11faa7d86b405e5c8f9881cd313336d518", size = 261510, upload-time = "2026-01-29T16:37:46.322Z" }, +] + +[[package]] +name = "nbformat" +version = "5.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastjsonschema" }, + { name = "jsonschema" }, + { name = "jupyter-core" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "notebook-shim" +version = "0.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/d2/92fa3243712b9a3e8bafaf60aac366da1cada3639ca767ff4b5b3654ec28/notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb", size = 13167, upload-time = "2024-02-14T23:35:18.353Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef", size = 13307, upload-time = "2024-02-14T23:35:16.286Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/44/71852273146957899753e69986246d6a176061ea183407e95418c2aa4d9a/numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825", size = 16955478, upload-time = "2026-01-31T23:10:25.623Z" }, + { url = "https://files.pythonhosted.org/packages/74/41/5d17d4058bd0cd96bcbd4d9ff0fb2e21f52702aab9a72e4a594efa18692f/numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1", size = 14965467, upload-time = "2026-01-31T23:10:28.186Z" }, + { url = "https://files.pythonhosted.org/packages/49/48/fb1ce8136c19452ed15f033f8aee91d5defe515094e330ce368a0647846f/numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7", size = 5475172, upload-time = "2026-01-31T23:10:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/40/a9/3feb49f17bbd1300dd2570432961f5c8a4ffeff1db6f02c7273bd020a4c9/numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73", size = 6805145, upload-time = "2026-01-31T23:10:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/3f/39/fdf35cbd6d6e2fcad42fcf85ac04a85a0d0fbfbf34b30721c98d602fd70a/numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1", size = 15966084, upload-time = "2026-01-31T23:10:34.502Z" }, + { url = "https://files.pythonhosted.org/packages/1b/46/6fa4ea94f1ddf969b2ee941290cca6f1bfac92b53c76ae5f44afe17ceb69/numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32", size = 16899477, upload-time = "2026-01-31T23:10:37.075Z" }, + { url = "https://files.pythonhosted.org/packages/09/a1/2a424e162b1a14a5bd860a464ab4e07513916a64ab1683fae262f735ccd2/numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390", size = 17323429, upload-time = "2026-01-31T23:10:39.704Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a2/73014149ff250628df72c58204822ac01d768697913881aacf839ff78680/numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413", size = 18635109, upload-time = "2026-01-31T23:10:41.924Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0c/73e8be2f1accd56df74abc1c5e18527822067dced5ec0861b5bb882c2ce0/numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda", size = 6237915, upload-time = "2026-01-31T23:10:45.26Z" }, + { url = "https://files.pythonhosted.org/packages/76/ae/e0265e0163cf127c24c3969d29f1c4c64551a1e375d95a13d32eab25d364/numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695", size = 12607972, upload-time = "2026-01-31T23:10:47.021Z" }, + { url = "https://files.pythonhosted.org/packages/29/a5/c43029af9b8014d6ea157f192652c50042e8911f4300f8f6ed3336bf437f/numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3", size = 10485763, upload-time = "2026-01-31T23:10:50.087Z" }, + { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" }, + { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" }, + { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" }, + { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" }, + { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" }, + { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" }, + { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" }, + { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" }, + { url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330, upload-time = "2026-01-31T23:11:23.958Z" }, + { url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968, upload-time = "2026-01-31T23:11:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311, upload-time = "2026-01-31T23:11:28.117Z" }, + { url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850, upload-time = "2026-01-31T23:11:30.888Z" }, + { url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210, upload-time = "2026-01-31T23:11:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199, upload-time = "2026-01-31T23:11:35.385Z" }, + { url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848, upload-time = "2026-01-31T23:11:38.001Z" }, + { url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082, upload-time = "2026-01-31T23:11:40.392Z" }, + { url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866, upload-time = "2026-01-31T23:11:42.495Z" }, + { url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631, upload-time = "2026-01-31T23:11:44.7Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254, upload-time = "2026-01-31T23:11:46.341Z" }, + { url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138, upload-time = "2026-01-31T23:11:48.082Z" }, + { url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398, upload-time = "2026-01-31T23:11:50.293Z" }, + { url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064, upload-time = "2026-01-31T23:11:52.927Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680, upload-time = "2026-01-31T23:11:55.22Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433, upload-time = "2026-01-31T23:11:58.096Z" }, + { url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181, upload-time = "2026-01-31T23:11:59.782Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756, upload-time = "2026-01-31T23:12:02.438Z" }, + { url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092, upload-time = "2026-01-31T23:12:04.521Z" }, + { url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770, upload-time = "2026-01-31T23:12:06.96Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562, upload-time = "2026-01-31T23:12:09.632Z" }, + { url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710, upload-time = "2026-01-31T23:12:11.969Z" }, + { url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205, upload-time = "2026-01-31T23:12:14.33Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738, upload-time = "2026-01-31T23:12:16.525Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888, upload-time = "2026-01-31T23:12:19.306Z" }, + { url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556, upload-time = "2026-01-31T23:12:21.816Z" }, + { url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899, upload-time = "2026-01-31T23:12:24.14Z" }, + { url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072, upload-time = "2026-01-31T23:12:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886, upload-time = "2026-01-31T23:12:28.488Z" }, + { url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567, upload-time = "2026-01-31T23:12:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372, upload-time = "2026-01-31T23:12:32.962Z" }, + { url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306, upload-time = "2026-01-31T23:12:34.797Z" }, + { url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394, upload-time = "2026-01-31T23:12:36.565Z" }, + { url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343, upload-time = "2026-01-31T23:12:39.188Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045, upload-time = "2026-01-31T23:12:42.041Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024, upload-time = "2026-01-31T23:12:44.331Z" }, + { url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937, upload-time = "2026-01-31T23:12:47.229Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844, upload-time = "2026-01-31T23:12:48.997Z" }, + { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/50e14d36d915ef64d8f8bc4a087fc8264d82c785eda6711f80ab7e620335/numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082", size = 16833179, upload-time = "2026-01-31T23:12:53.5Z" }, + { url = "https://files.pythonhosted.org/packages/17/17/809b5cad63812058a8189e91a1e2d55a5a18fd04611dbad244e8aeae465c/numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a", size = 14889755, upload-time = "2026-01-31T23:12:55.933Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ea/181b9bcf7627fc8371720316c24db888dcb9829b1c0270abf3d288b2e29b/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920", size = 5399500, upload-time = "2026-01-31T23:12:58.671Z" }, + { url = "https://files.pythonhosted.org/packages/33/9f/413adf3fc955541ff5536b78fcf0754680b3c6d95103230252a2c9408d23/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821", size = 6714252, upload-time = "2026-01-31T23:13:00.518Z" }, + { url = "https://files.pythonhosted.org/packages/91/da/643aad274e29ccbdf42ecd94dafe524b81c87bcb56b83872d54827f10543/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb", size = 15797142, upload-time = "2026-01-31T23:13:02.219Z" }, + { url = "https://files.pythonhosted.org/packages/66/27/965b8525e9cb5dc16481b30a1b3c21e50c7ebf6e9dbd48d0c4d0d5089c7e/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0", size = 16727979, upload-time = "2026-01-31T23:13:04.62Z" }, + { url = "https://files.pythonhosted.org/packages/de/e5/b7d20451657664b07986c2f6e3be564433f5dcaf3482d68eaecd79afaf03/numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", size = 12502577, upload-time = "2026-01-31T23:13:07.08Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, +] + +[[package]] +name = "overrides" +version = "7.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812, upload-time = "2024-01-27T21:01:33.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832, upload-time = "2024-01-27T21:01:31.393Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pandas" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/da/b1dc0481ab8d55d0f46e343cfe67d4551a0e14fcee52bd38ca1bd73258d8/pandas-3.0.0.tar.gz", hash = "sha256:0facf7e87d38f721f0af46fe70d97373a37701b1c09f7ed7aeeb292ade5c050f", size = 4633005, upload-time = "2026-01-21T15:52:04.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/1e/b184654a856e75e975a6ee95d6577b51c271cd92cb2b020c9378f53e0032/pandas-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d64ce01eb9cdca96a15266aa679ae50212ec52757c79204dbc7701a222401850", size = 10313247, upload-time = "2026-01-21T15:50:15.775Z" }, + { url = "https://files.pythonhosted.org/packages/dd/5e/e04a547ad0f0183bf151fd7c7a477468e3b85ff2ad231c566389e6cc9587/pandas-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:613e13426069793aa1ec53bdcc3b86e8d32071daea138bbcf4fa959c9cdaa2e2", size = 9913131, upload-time = "2026-01-21T15:50:18.611Z" }, + { url = "https://files.pythonhosted.org/packages/a2/93/bb77bfa9fc2aba9f7204db807d5d3fb69832ed2854c60ba91b4c65ba9219/pandas-3.0.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0192fee1f1a8e743b464a6607858ee4b071deb0b118eb143d71c2a1d170996d5", size = 10741925, upload-time = "2026-01-21T15:50:21.058Z" }, + { url = "https://files.pythonhosted.org/packages/62/fb/89319812eb1d714bfc04b7f177895caeba8ab4a37ef6712db75ed786e2e0/pandas-3.0.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0b853319dec8d5e0c8b875374c078ef17f2269986a78168d9bd57e49bf650ae", size = 11245979, upload-time = "2026-01-21T15:50:23.413Z" }, + { url = "https://files.pythonhosted.org/packages/a9/63/684120486f541fc88da3862ed31165b3b3e12b6a1c7b93be4597bc84e26c/pandas-3.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:707a9a877a876c326ae2cb640fbdc4ef63b0a7b9e2ef55c6df9942dcee8e2af9", size = 11756337, upload-time = "2026-01-21T15:50:25.932Z" }, + { url = "https://files.pythonhosted.org/packages/39/92/7eb0ad232312b59aec61550c3c81ad0743898d10af5df7f80bc5e5065416/pandas-3.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:afd0aa3d0b5cda6e0b8ffc10dbcca3b09ef3cbcd3fe2b27364f85fdc04e1989d", size = 12325517, upload-time = "2026-01-21T15:50:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/51/27/bf9436dd0a4fc3130acec0828951c7ef96a0631969613a9a35744baf27f6/pandas-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:113b4cca2614ff7e5b9fee9b6f066618fe73c5a83e99d721ffc41217b2bf57dd", size = 9881576, upload-time = "2026-01-21T15:50:30.149Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2b/c618b871fce0159fd107516336e82891b404e3f340821853c2fc28c7830f/pandas-3.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c14837eba8e99a8da1527c0280bba29b0eb842f64aa94982c5e21227966e164b", size = 9140807, upload-time = "2026-01-21T15:50:32.308Z" }, + { url = "https://files.pythonhosted.org/packages/0b/38/db33686f4b5fa64d7af40d96361f6a4615b8c6c8f1b3d334eee46ae6160e/pandas-3.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9803b31f5039b3c3b10cc858c5e40054adb4b29b4d81cb2fd789f4121c8efbcd", size = 10334013, upload-time = "2026-01-21T15:50:34.771Z" }, + { url = "https://files.pythonhosted.org/packages/a5/7b/9254310594e9774906bacdd4e732415e1f86ab7dbb4b377ef9ede58cd8ec/pandas-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14c2a4099cd38a1d18ff108168ea417909b2dea3bd1ebff2ccf28ddb6a74d740", size = 9874154, upload-time = "2026-01-21T15:50:36.67Z" }, + { url = "https://files.pythonhosted.org/packages/63/d4/726c5a67a13bc66643e66d2e9ff115cead482a44fc56991d0c4014f15aaf/pandas-3.0.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d257699b9a9960e6125686098d5714ac59d05222bef7a5e6af7a7fd87c650801", size = 10384433, upload-time = "2026-01-21T15:50:39.132Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2e/9211f09bedb04f9832122942de8b051804b31a39cfbad199a819bb88d9f3/pandas-3.0.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:69780c98f286076dcafca38d8b8eee1676adf220199c0a39f0ecbf976b68151a", size = 10864519, upload-time = "2026-01-21T15:50:41.043Z" }, + { url = "https://files.pythonhosted.org/packages/00/8d/50858522cdc46ac88b9afdc3015e298959a70a08cd21e008a44e9520180c/pandas-3.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4a66384f017240f3858a4c8a7cf21b0591c3ac885cddb7758a589f0f71e87ebb", size = 11394124, upload-time = "2026-01-21T15:50:43.377Z" }, + { url = "https://files.pythonhosted.org/packages/86/3f/83b2577db02503cd93d8e95b0f794ad9d4be0ba7cb6c8bcdcac964a34a42/pandas-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be8c515c9bc33989d97b89db66ea0cececb0f6e3c2a87fcc8b69443a6923e95f", size = 11920444, upload-time = "2026-01-21T15:50:45.932Z" }, + { url = "https://files.pythonhosted.org/packages/64/2d/4f8a2f192ed12c90a0aab47f5557ece0e56b0370c49de9454a09de7381b2/pandas-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:a453aad8c4f4e9f166436994a33884442ea62aa8b27d007311e87521b97246e1", size = 9730970, upload-time = "2026-01-21T15:50:47.962Z" }, + { url = "https://files.pythonhosted.org/packages/d4/64/ff571be435cf1e643ca98d0945d76732c0b4e9c37191a89c8550b105eed1/pandas-3.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:da768007b5a33057f6d9053563d6b74dd6d029c337d93c6d0d22a763a5c2ecc0", size = 9041950, upload-time = "2026-01-21T15:50:50.422Z" }, + { url = "https://files.pythonhosted.org/packages/6f/fa/7f0ac4ca8877c57537aaff2a842f8760e630d8e824b730eb2e859ffe96ca/pandas-3.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b78d646249b9a2bc191040988c7bb524c92fa8534fb0898a0741d7e6f2ffafa6", size = 10307129, upload-time = "2026-01-21T15:50:52.877Z" }, + { url = "https://files.pythonhosted.org/packages/6f/11/28a221815dcea4c0c9414dfc845e34a84a6a7dabc6da3194498ed5ba4361/pandas-3.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bc9cba7b355cb4162442a88ce495e01cb605f17ac1e27d6596ac963504e0305f", size = 9850201, upload-time = "2026-01-21T15:50:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/ba/da/53bbc8c5363b7e5bd10f9ae59ab250fc7a382ea6ba08e4d06d8694370354/pandas-3.0.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c9a1a149aed3b6c9bf246033ff91e1b02d529546c5d6fb6b74a28fea0cf4c70", size = 10354031, upload-time = "2026-01-21T15:50:57.463Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a3/51e02ebc2a14974170d51e2410dfdab58870ea9bcd37cda15bd553d24dc4/pandas-3.0.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95683af6175d884ee89471842acfca29172a85031fccdabc35e50c0984470a0e", size = 10861165, upload-time = "2026-01-21T15:50:59.32Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fe/05a51e3cac11d161472b8297bd41723ea98013384dd6d76d115ce3482f9b/pandas-3.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1fbbb5a7288719e36b76b4f18d46ede46e7f916b6c8d9915b756b0a6c3f792b3", size = 11359359, upload-time = "2026-01-21T15:51:02.014Z" }, + { url = "https://files.pythonhosted.org/packages/ee/56/ba620583225f9b85a4d3e69c01df3e3870659cc525f67929b60e9f21dcd1/pandas-3.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e8b9808590fa364416b49b2a35c1f4cf2785a6c156935879e57f826df22038e", size = 11912907, upload-time = "2026-01-21T15:51:05.175Z" }, + { url = "https://files.pythonhosted.org/packages/c9/8c/c6638d9f67e45e07656b3826405c5cc5f57f6fd07c8b2572ade328c86e22/pandas-3.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:98212a38a709feb90ae658cb6227ea3657c22ba8157d4b8f913cd4c950de5e7e", size = 9732138, upload-time = "2026-01-21T15:51:07.569Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bf/bd1335c3bf1770b6d8fed2799993b11c4971af93bb1b729b9ebbc02ca2ec/pandas-3.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:177d9df10b3f43b70307a149d7ec49a1229a653f907aa60a48f1877d0e6be3be", size = 9033568, upload-time = "2026-01-21T15:51:09.484Z" }, + { url = "https://files.pythonhosted.org/packages/8e/c6/f5e2171914d5e29b9171d495344097d54e3ffe41d2d85d8115baba4dc483/pandas-3.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2713810ad3806767b89ad3b7b69ba153e1c6ff6d9c20f9c2140379b2a98b6c98", size = 10741936, upload-time = "2026-01-21T15:51:11.693Z" }, + { url = "https://files.pythonhosted.org/packages/51/88/9a0164f99510a1acb9f548691f022c756c2314aad0d8330a24616c14c462/pandas-3.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:15d59f885ee5011daf8335dff47dcb8a912a27b4ad7826dc6cbe809fd145d327", size = 10393884, upload-time = "2026-01-21T15:51:14.197Z" }, + { url = "https://files.pythonhosted.org/packages/e0/53/b34d78084d88d8ae2b848591229da8826d1e65aacf00b3abe34023467648/pandas-3.0.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24e6547fb64d2c92665dd2adbfa4e85fa4fd70a9c070e7cfb03b629a0bbab5eb", size = 10310740, upload-time = "2026-01-21T15:51:16.093Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d3/bee792e7c3d6930b74468d990604325701412e55d7aaf47460a22311d1a5/pandas-3.0.0-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48ee04b90e2505c693d3f8e8f524dab8cb8aaf7ddcab52c92afa535e717c4812", size = 10700014, upload-time = "2026-01-21T15:51:18.818Z" }, + { url = "https://files.pythonhosted.org/packages/55/db/2570bc40fb13aaed1cbc3fbd725c3a60ee162477982123c3adc8971e7ac1/pandas-3.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66f72fb172959af42a459e27a8d8d2c7e311ff4c1f7db6deb3b643dbc382ae08", size = 11323737, upload-time = "2026-01-21T15:51:20.784Z" }, + { url = "https://files.pythonhosted.org/packages/bc/2e/297ac7f21c8181b62a4cccebad0a70caf679adf3ae5e83cb676194c8acc3/pandas-3.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4a4a400ca18230976724a5066f20878af785f36c6756e498e94c2a5e5d57779c", size = 11771558, upload-time = "2026-01-21T15:51:22.977Z" }, + { url = "https://files.pythonhosted.org/packages/0a/46/e1c6876d71c14332be70239acce9ad435975a80541086e5ffba2f249bcf6/pandas-3.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:940eebffe55528074341a5a36515f3e4c5e25e958ebbc764c9502cfc35ba3faa", size = 10473771, upload-time = "2026-01-21T15:51:25.285Z" }, + { url = "https://files.pythonhosted.org/packages/c0/db/0270ad9d13c344b7a36fa77f5f8344a46501abf413803e885d22864d10bf/pandas-3.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:597c08fb9fef0edf1e4fa2f9828dd27f3d78f9b8c9b4a748d435ffc55732310b", size = 10312075, upload-time = "2026-01-21T15:51:28.5Z" }, + { url = "https://files.pythonhosted.org/packages/09/9f/c176f5e9717f7c91becfe0f55a52ae445d3f7326b4a2cf355978c51b7913/pandas-3.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:447b2d68ac5edcbf94655fe909113a6dba6ef09ad7f9f60c80477825b6c489fe", size = 9900213, upload-time = "2026-01-21T15:51:30.955Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e7/63ad4cc10b257b143e0a5ebb04304ad806b4e1a61c5da25f55896d2ca0f4/pandas-3.0.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:debb95c77ff3ed3ba0d9aa20c3a2f19165cc7956362f9873fce1ba0a53819d70", size = 10428768, upload-time = "2026-01-21T15:51:33.018Z" }, + { url = "https://files.pythonhosted.org/packages/9e/0e/4e4c2d8210f20149fd2248ef3fff26623604922bd564d915f935a06dd63d/pandas-3.0.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fedabf175e7cd82b69b74c30adbaa616de301291a5231138d7242596fc296a8d", size = 10882954, upload-time = "2026-01-21T15:51:35.287Z" }, + { url = "https://files.pythonhosted.org/packages/c6/60/c9de8ac906ba1f4d2250f8a951abe5135b404227a55858a75ad26f84db47/pandas-3.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:412d1a89aab46889f3033a386912efcdfa0f1131c5705ff5b668dda88305e986", size = 11430293, upload-time = "2026-01-21T15:51:37.57Z" }, + { url = "https://files.pythonhosted.org/packages/a1/69/806e6637c70920e5787a6d6896fd707f8134c2c55cd761e7249a97b7dc5a/pandas-3.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e979d22316f9350c516479dd3a92252be2937a9531ed3a26ec324198a99cdd49", size = 11952452, upload-time = "2026-01-21T15:51:39.618Z" }, + { url = "https://files.pythonhosted.org/packages/cb/de/918621e46af55164c400ab0ef389c9d969ab85a43d59ad1207d4ddbe30a5/pandas-3.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:083b11415b9970b6e7888800c43c82e81a06cd6b06755d84804444f0007d6bb7", size = 9851081, upload-time = "2026-01-21T15:51:41.758Z" }, + { url = "https://files.pythonhosted.org/packages/91/a1/3562a18dd0bd8c73344bfa26ff90c53c72f827df119d6d6b1dacc84d13e3/pandas-3.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:5db1e62cb99e739fa78a28047e861b256d17f88463c76b8dafc7c1338086dca8", size = 9174610, upload-time = "2026-01-21T15:51:44.312Z" }, + { url = "https://files.pythonhosted.org/packages/ce/26/430d91257eaf366f1737d7a1c158677caaf6267f338ec74e3a1ec444111c/pandas-3.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:697b8f7d346c68274b1b93a170a70974cdc7d7354429894d5927c1effdcccd73", size = 10761999, upload-time = "2026-01-21T15:51:46.899Z" }, + { url = "https://files.pythonhosted.org/packages/ec/1a/954eb47736c2b7f7fe6a9d56b0cb6987773c00faa3c6451a43db4beb3254/pandas-3.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8cb3120f0d9467ed95e77f67a75e030b67545bcfa08964e349252d674171def2", size = 10410279, upload-time = "2026-01-21T15:51:48.89Z" }, + { url = "https://files.pythonhosted.org/packages/20/fc/b96f3a5a28b250cd1b366eb0108df2501c0f38314a00847242abab71bb3a/pandas-3.0.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33fd3e6baa72899746b820c31e4b9688c8e1b7864d7aec2de7ab5035c285277a", size = 10330198, upload-time = "2026-01-21T15:51:51.015Z" }, + { url = "https://files.pythonhosted.org/packages/90/b3/d0e2952f103b4fbef1ef22d0c2e314e74fc9064b51cee30890b5e3286ee6/pandas-3.0.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8942e333dc67ceda1095227ad0febb05a3b36535e520154085db632c40ad084", size = 10728513, upload-time = "2026-01-21T15:51:53.387Z" }, + { url = "https://files.pythonhosted.org/packages/76/81/832894f286df828993dc5fd61c63b231b0fb73377e99f6c6c369174cf97e/pandas-3.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:783ac35c4d0fe0effdb0d67161859078618b1b6587a1af15928137525217a721", size = 11345550, upload-time = "2026-01-21T15:51:55.329Z" }, + { url = "https://files.pythonhosted.org/packages/34/a0/ed160a00fb4f37d806406bc0a79a8b62fe67f29d00950f8d16203ff3409b/pandas-3.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:125eb901e233f155b268bbef9abd9afb5819db74f0e677e89a61b246228c71ac", size = 11799386, upload-time = "2026-01-21T15:51:57.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/c8/2ac00d7255252c5e3cf61b35ca92ca25704b0188f7454ca4aec08a33cece/pandas-3.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b86d113b6c109df3ce0ad5abbc259fe86a1bd4adfd4a31a89da42f84f65509bb", size = 10873041, upload-time = "2026-01-21T15:52:00.034Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3f/a80ac00acbc6b35166b42850e98a4f466e2c0d9c64054161ba9620f95680/pandas-3.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1c39eab3ad38f2d7a249095f0a3d8f8c22cc0f847e98ccf5bbe732b272e2d9fa", size = 9441003, upload-time = "2026-01-21T15:52:02.281Z" }, +] + +[[package]] +name = "pandoc" +version = "2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "plumbum" }, + { name = "ply" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/9a/e3186e760c57ee5f1c27ea5cea577a0ff9abfca51eefcb4d9a4cd39aff2e/pandoc-2.4.tar.gz", hash = "sha256:ecd1f8cbb7f4180c6b5db4a17a7c1a74df519995f5f186ef81ce72a9cbd0dd9a", size = 34635, upload-time = "2024-08-07T14:33:58.016Z" } + +[[package]] +name = "pandocfilters" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454, upload-time = "2024-01-18T20:08:13.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663, upload-time = "2024-01-18T20:08:11.28Z" }, +] + +[[package]] +name = "panel" +version = "1.8.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bleach" }, + { name = "bokeh" }, + { name = "linkify-it-py" }, + { name = "markdown" }, + { name = "markdown-it-py" }, + { name = "mdit-py-plugins" }, + { name = "narwhals" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "param" }, + { name = "pyviz-comms" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/39/e34d76638e8312d79855dd8f3bbce2b07484031bac4b791503ac47ce3daf/panel-1.8.7.tar.gz", hash = "sha256:76c9822e899ee08b945e562c3ae8e028e508019fd61ba0129abbf24d02ea031d", size = 32135803, upload-time = "2026-01-28T16:52:52.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/75/689e2ebb7fca5c7c67ce2e2a91538b66b75a3e27a7496aa9ed96c349b2a6/panel-1.8.7-py3-none-any.whl", hash = "sha256:6cc60a8b5497628a896b935706701ff9b640ed001f6a48d0bd67163938b546da", size = 30223006, upload-time = "2026-01-28T16:52:49.518Z" }, +] + +[[package]] +name = "param" +version = "2.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/bc/6f3e6a2cbde006f9a5a4591fd87ec71ee8007252e93bc23b803d1cfa043a/param-2.3.2.tar.gz", hash = "sha256:ec70669bda9a3c13491098e7f5b640f60022b58b2f5bc7997099d54ea237d1de", size = 201733, upload-time = "2026-02-06T17:41:48.116Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/b6/f8c7e1f5f716e16070cf35f90c24f95f397376bb810e65000b6bc55950cc/param-2.3.2-py3-none-any.whl", hash = "sha256:147717b21cf2d8add08edb135f678c5fda08a701dc69e0897d75812e4c2af365", size = 139763, upload-time = "2026-02-06T17:41:46.792Z" }, +] + +[[package]] +name = "parso" +version = "0.8.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/76/a1e769043c0c0c9fe391b702539d594731a4362334cdf4dc25d0c09761e7/parso-0.8.6.tar.gz", hash = "sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd", size = 401621, upload-time = "2026-02-09T15:45:24.425Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" }, +] + +[[package]] +name = "partd" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "locket" }, + { name = "toolz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/3a/3f06f34820a31257ddcabdfafc2672c5816be79c7e353b02c1f318daa7d4/partd-1.4.2.tar.gz", hash = "sha256:d022c33afbdc8405c226621b015e8067888173d85f7f5ecebb3cafed9a20f02c", size = 21029, upload-time = "2024-05-06T19:51:41.945Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl", hash = "sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f", size = 18905, upload-time = "2024-05-06T19:51:39.271Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + +[[package]] +name = "pillow" +version = "12.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/c4/bf8328039de6cc22182c3ef007a2abfbbdab153661c0a9aa78af8d706391/pillow-12.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3", size = 5304057, upload-time = "2026-01-02T09:10:46.627Z" }, + { url = "https://files.pythonhosted.org/packages/43/06/7264c0597e676104cc22ca73ee48f752767cd4b1fe084662620b17e10120/pillow-12.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0", size = 4657811, upload-time = "2026-01-02T09:10:49.548Z" }, + { url = "https://files.pythonhosted.org/packages/72/64/f9189e44474610daf83da31145fa56710b627b5c4c0b9c235e34058f6b31/pillow-12.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451", size = 6232243, upload-time = "2026-01-02T09:10:51.62Z" }, + { url = "https://files.pythonhosted.org/packages/ef/30/0df458009be6a4caca4ca2c52975e6275c387d4e5c95544e34138b41dc86/pillow-12.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e", size = 8037872, upload-time = "2026-01-02T09:10:53.446Z" }, + { url = "https://files.pythonhosted.org/packages/e4/86/95845d4eda4f4f9557e25381d70876aa213560243ac1a6d619c46caaedd9/pillow-12.1.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84", size = 6345398, upload-time = "2026-01-02T09:10:55.426Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1f/8e66ab9be3aaf1435bc03edd1ebdf58ffcd17f7349c1d970cafe87af27d9/pillow-12.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0", size = 7034667, upload-time = "2026-01-02T09:10:57.11Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f6/683b83cb9b1db1fb52b87951b1c0b99bdcfceaa75febf11406c19f82cb5e/pillow-12.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b", size = 6458743, upload-time = "2026-01-02T09:10:59.331Z" }, + { url = "https://files.pythonhosted.org/packages/9a/7d/de833d63622538c1d58ce5395e7c6cb7e7dce80decdd8bde4a484e095d9f/pillow-12.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18", size = 7159342, upload-time = "2026-01-02T09:11:01.82Z" }, + { url = "https://files.pythonhosted.org/packages/8c/40/50d86571c9e5868c42b81fe7da0c76ca26373f3b95a8dd675425f4a92ec1/pillow-12.1.0-cp311-cp311-win32.whl", hash = "sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64", size = 6328655, upload-time = "2026-01-02T09:11:04.556Z" }, + { url = "https://files.pythonhosted.org/packages/6c/af/b1d7e301c4cd26cd45d4af884d9ee9b6fab893b0ad2450d4746d74a6968c/pillow-12.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75", size = 7031469, upload-time = "2026-01-02T09:11:06.538Z" }, + { url = "https://files.pythonhosted.org/packages/48/36/d5716586d887fb2a810a4a61518a327a1e21c8b7134c89283af272efe84b/pillow-12.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304", size = 2452515, upload-time = "2026-01-02T09:11:08.226Z" }, + { url = "https://files.pythonhosted.org/packages/20/31/dc53fe21a2f2996e1b7d92bf671cdb157079385183ef7c1ae08b485db510/pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b", size = 5262642, upload-time = "2026-01-02T09:11:10.138Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c1/10e45ac9cc79419cedf5121b42dcca5a50ad2b601fa080f58c22fb27626e/pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551", size = 4657464, upload-time = "2026-01-02T09:11:12.319Z" }, + { url = "https://files.pythonhosted.org/packages/ad/26/7b82c0ab7ef40ebede7a97c72d473bda5950f609f8e0c77b04af574a0ddb/pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208", size = 6234878, upload-time = "2026-01-02T09:11:14.096Z" }, + { url = "https://files.pythonhosted.org/packages/76/25/27abc9792615b5e886ca9411ba6637b675f1b77af3104710ac7353fe5605/pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5", size = 8044868, upload-time = "2026-01-02T09:11:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ea/f200a4c36d836100e7bc738fc48cd963d3ba6372ebc8298a889e0cfc3359/pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661", size = 6349468, upload-time = "2026-01-02T09:11:17.631Z" }, + { url = "https://files.pythonhosted.org/packages/11/8f/48d0b77ab2200374c66d344459b8958c86693be99526450e7aee714e03e4/pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17", size = 7041518, upload-time = "2026-01-02T09:11:19.389Z" }, + { url = "https://files.pythonhosted.org/packages/1d/23/c281182eb986b5d31f0a76d2a2c8cd41722d6fb8ed07521e802f9bba52de/pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670", size = 6462829, upload-time = "2026-01-02T09:11:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/25/ef/7018273e0faac099d7b00982abdcc39142ae6f3bd9ceb06de09779c4a9d6/pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616", size = 7166756, upload-time = "2026-01-02T09:11:23.559Z" }, + { url = "https://files.pythonhosted.org/packages/8f/c8/993d4b7ab2e341fe02ceef9576afcf5830cdec640be2ac5bee1820d693d4/pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7", size = 6328770, upload-time = "2026-01-02T09:11:25.661Z" }, + { url = "https://files.pythonhosted.org/packages/a7/87/90b358775a3f02765d87655237229ba64a997b87efa8ccaca7dd3e36e7a7/pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d", size = 7033406, upload-time = "2026-01-02T09:11:27.474Z" }, + { url = "https://files.pythonhosted.org/packages/5d/cf/881b457eccacac9e5b2ddd97d5071fb6d668307c57cbf4e3b5278e06e536/pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c", size = 2452612, upload-time = "2026-01-02T09:11:29.309Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c7/2530a4aa28248623e9d7f27316b42e27c32ec410f695929696f2e0e4a778/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1", size = 4062543, upload-time = "2026-01-02T09:11:31.566Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1f/40b8eae823dc1519b87d53c30ed9ef085506b05281d313031755c1705f73/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179", size = 4138373, upload-time = "2026-01-02T09:11:33.367Z" }, + { url = "https://files.pythonhosted.org/packages/d4/77/6fa60634cf06e52139fd0e89e5bbf055e8166c691c42fb162818b7fda31d/pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0", size = 3601241, upload-time = "2026-01-02T09:11:35.011Z" }, + { url = "https://files.pythonhosted.org/packages/4f/bf/28ab865de622e14b747f0cd7877510848252d950e43002e224fb1c9ababf/pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587", size = 5262410, upload-time = "2026-01-02T09:11:36.682Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/583420a1b55e715937a85bd48c5c0991598247a1fd2eb5423188e765ea02/pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac", size = 4657312, upload-time = "2026-01-02T09:11:38.535Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fd/f5a0896839762885b3376ff04878f86ab2b097c2f9a9cdccf4eda8ba8dc0/pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b", size = 6232605, upload-time = "2026-01-02T09:11:40.602Z" }, + { url = "https://files.pythonhosted.org/packages/98/aa/938a09d127ac1e70e6ed467bd03834350b33ef646b31edb7452d5de43792/pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea", size = 8041617, upload-time = "2026-01-02T09:11:42.721Z" }, + { url = "https://files.pythonhosted.org/packages/17/e8/538b24cb426ac0186e03f80f78bc8dc7246c667f58b540bdd57c71c9f79d/pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c", size = 6346509, upload-time = "2026-01-02T09:11:44.955Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/632e58ec89a32738cabfd9ec418f0e9898a2b4719afc581f07c04a05e3c9/pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc", size = 7038117, upload-time = "2026-01-02T09:11:46.736Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a2/d40308cf86eada842ca1f3ffa45d0ca0df7e4ab33c83f81e73f5eaed136d/pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644", size = 6460151, upload-time = "2026-01-02T09:11:48.625Z" }, + { url = "https://files.pythonhosted.org/packages/f1/88/f5b058ad6453a085c5266660a1417bdad590199da1b32fb4efcff9d33b05/pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c", size = 7164534, upload-time = "2026-01-02T09:11:50.445Z" }, + { url = "https://files.pythonhosted.org/packages/19/ce/c17334caea1db789163b5d855a5735e47995b0b5dc8745e9a3605d5f24c0/pillow-12.1.0-cp313-cp313-win32.whl", hash = "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171", size = 6332551, upload-time = "2026-01-02T09:11:52.234Z" }, + { url = "https://files.pythonhosted.org/packages/e5/07/74a9d941fa45c90a0d9465098fe1ec85de3e2afbdc15cc4766622d516056/pillow-12.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a", size = 7040087, upload-time = "2026-01-02T09:11:54.822Z" }, + { url = "https://files.pythonhosted.org/packages/88/09/c99950c075a0e9053d8e880595926302575bc742b1b47fe1bbcc8d388d50/pillow-12.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45", size = 2452470, upload-time = "2026-01-02T09:11:56.522Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ba/970b7d85ba01f348dee4d65412476321d40ee04dcb51cd3735b9dc94eb58/pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d", size = 5264816, upload-time = "2026-01-02T09:11:58.227Z" }, + { url = "https://files.pythonhosted.org/packages/10/60/650f2fb55fdba7a510d836202aa52f0baac633e50ab1cf18415d332188fb/pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0", size = 4660472, upload-time = "2026-01-02T09:12:00.798Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/5273a99478956a099d533c4f46cbaa19fd69d606624f4334b85e50987a08/pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554", size = 6268974, upload-time = "2026-01-02T09:12:02.572Z" }, + { url = "https://files.pythonhosted.org/packages/b4/26/0bf714bc2e73d5267887d47931d53c4ceeceea6978148ed2ab2a4e6463c4/pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e", size = 8073070, upload-time = "2026-01-02T09:12:04.75Z" }, + { url = "https://files.pythonhosted.org/packages/43/cf/1ea826200de111a9d65724c54f927f3111dc5ae297f294b370a670c17786/pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82", size = 6380176, upload-time = "2026-01-02T09:12:06.626Z" }, + { url = "https://files.pythonhosted.org/packages/03/e0/7938dd2b2013373fd85d96e0f38d62b7a5a262af21ac274250c7ca7847c9/pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4", size = 7067061, upload-time = "2026-01-02T09:12:08.624Z" }, + { url = "https://files.pythonhosted.org/packages/86/ad/a2aa97d37272a929a98437a8c0ac37b3cf012f4f8721e1bd5154699b2518/pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0", size = 6491824, upload-time = "2026-01-02T09:12:10.488Z" }, + { url = "https://files.pythonhosted.org/packages/a4/44/80e46611b288d51b115826f136fb3465653c28f491068a72d3da49b54cd4/pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b", size = 7190911, upload-time = "2026-01-02T09:12:12.772Z" }, + { url = "https://files.pythonhosted.org/packages/86/77/eacc62356b4cf81abe99ff9dbc7402750044aed02cfd6a503f7c6fc11f3e/pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65", size = 6336445, upload-time = "2026-01-02T09:12:14.775Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3c/57d81d0b74d218706dafccb87a87ea44262c43eef98eb3b164fd000e0491/pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0", size = 7045354, upload-time = "2026-01-02T09:12:16.599Z" }, + { url = "https://files.pythonhosted.org/packages/ac/82/8b9b97bba2e3576a340f93b044a3a3a09841170ab4c1eb0d5c93469fd32f/pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8", size = 2454547, upload-time = "2026-01-02T09:12:18.704Z" }, + { url = "https://files.pythonhosted.org/packages/8c/87/bdf971d8bbcf80a348cc3bacfcb239f5882100fe80534b0ce67a784181d8/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91", size = 4062533, upload-time = "2026-01-02T09:12:20.791Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4f/5eb37a681c68d605eb7034c004875c81f86ec9ef51f5be4a63eadd58859a/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796", size = 4138546, upload-time = "2026-01-02T09:12:23.664Z" }, + { url = "https://files.pythonhosted.org/packages/11/6d/19a95acb2edbace40dcd582d077b991646b7083c41b98da4ed7555b59733/pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd", size = 3601163, upload-time = "2026-01-02T09:12:26.338Z" }, + { url = "https://files.pythonhosted.org/packages/fc/36/2b8138e51cb42e4cc39c3297713455548be855a50558c3ac2beebdc251dd/pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13", size = 5266086, upload-time = "2026-01-02T09:12:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/649056e4d22e1caa90816bf99cef0884aed607ed38075bd75f091a607a38/pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e", size = 4657344, upload-time = "2026-01-02T09:12:31.117Z" }, + { url = "https://files.pythonhosted.org/packages/6c/6b/c5742cea0f1ade0cd61485dc3d81f05261fc2276f537fbdc00802de56779/pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643", size = 6232114, upload-time = "2026-01-02T09:12:32.936Z" }, + { url = "https://files.pythonhosted.org/packages/bf/8f/9f521268ce22d63991601aafd3d48d5ff7280a246a1ef62d626d67b44064/pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5", size = 8042708, upload-time = "2026-01-02T09:12:34.78Z" }, + { url = "https://files.pythonhosted.org/packages/1a/eb/257f38542893f021502a1bbe0c2e883c90b5cff26cc33b1584a841a06d30/pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de", size = 6347762, upload-time = "2026-01-02T09:12:36.748Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5a/8ba375025701c09b309e8d5163c5a4ce0102fa86bbf8800eb0d7ac87bc51/pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9", size = 7039265, upload-time = "2026-01-02T09:12:39.082Z" }, + { url = "https://files.pythonhosted.org/packages/cf/dc/cf5e4cdb3db533f539e88a7bbf9f190c64ab8a08a9bc7a4ccf55067872e4/pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a", size = 6462341, upload-time = "2026-01-02T09:12:40.946Z" }, + { url = "https://files.pythonhosted.org/packages/d0/47/0291a25ac9550677e22eda48510cfc4fa4b2ef0396448b7fbdc0a6946309/pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a", size = 7165395, upload-time = "2026-01-02T09:12:42.706Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4c/e005a59393ec4d9416be06e6b45820403bb946a778e39ecec62f5b2b991e/pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030", size = 6431413, upload-time = "2026-01-02T09:12:44.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/af/f23697f587ac5f9095d67e31b81c95c0249cd461a9798a061ed6709b09b5/pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94", size = 7176779, upload-time = "2026-01-02T09:12:46.727Z" }, + { url = "https://files.pythonhosted.org/packages/b3/36/6a51abf8599232f3e9afbd16d52829376a68909fe14efe29084445db4b73/pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4", size = 2543105, upload-time = "2026-01-02T09:12:49.243Z" }, + { url = "https://files.pythonhosted.org/packages/82/54/2e1dd20c8749ff225080d6ba465a0cab4387f5db0d1c5fb1439e2d99923f/pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2", size = 5268571, upload-time = "2026-01-02T09:12:51.11Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/571163a5ef86ec0cf30d265ac2a70ae6fc9e28413d1dc94fa37fae6bda89/pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61", size = 4660426, upload-time = "2026-01-02T09:12:52.865Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e1/53ee5163f794aef1bf84243f755ee6897a92c708505350dd1923f4afec48/pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51", size = 6269908, upload-time = "2026-01-02T09:12:54.884Z" }, + { url = "https://files.pythonhosted.org/packages/bc/0b/b4b4106ff0ee1afa1dc599fde6ab230417f800279745124f6c50bcffed8e/pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc", size = 8074733, upload-time = "2026-01-02T09:12:56.802Z" }, + { url = "https://files.pythonhosted.org/packages/19/9f/80b411cbac4a732439e629a26ad3ef11907a8c7fc5377b7602f04f6fe4e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14", size = 6381431, upload-time = "2026-01-02T09:12:58.823Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b7/d65c45db463b66ecb6abc17c6ba6917a911202a07662247e1355ce1789e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8", size = 7068529, upload-time = "2026-01-02T09:13:00.885Z" }, + { url = "https://files.pythonhosted.org/packages/50/96/dfd4cd726b4a45ae6e3c669fc9e49deb2241312605d33aba50499e9d9bd1/pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924", size = 6492981, upload-time = "2026-01-02T09:13:03.314Z" }, + { url = "https://files.pythonhosted.org/packages/4d/1c/b5dc52cf713ae46033359c5ca920444f18a6359ce1020dd3e9c553ea5bc6/pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef", size = 7191878, upload-time = "2026-01-02T09:13:05.276Z" }, + { url = "https://files.pythonhosted.org/packages/53/26/c4188248bd5edaf543864fe4834aebe9c9cb4968b6f573ce014cc42d0720/pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988", size = 6438703, upload-time = "2026-01-02T09:13:07.491Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0e/69ed296de8ea05cb03ee139cee600f424ca166e632567b2d66727f08c7ed/pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6", size = 7182927, upload-time = "2026-01-02T09:13:09.841Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f5/68334c015eed9b5cff77814258717dec591ded209ab5b6fb70e2ae873d1d/pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831", size = 2545104, upload-time = "2026-01-02T09:13:12.068Z" }, + { url = "https://files.pythonhosted.org/packages/8b/bc/224b1d98cffd7164b14707c91aac83c07b047fbd8f58eba4066a3e53746a/pillow-12.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377", size = 5228605, upload-time = "2026-01-02T09:13:14.084Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ca/49ca7769c4550107de049ed85208240ba0f330b3f2e316f24534795702ce/pillow-12.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72", size = 4622245, upload-time = "2026-01-02T09:13:15.964Z" }, + { url = "https://files.pythonhosted.org/packages/73/48/fac807ce82e5955bcc2718642b94b1bd22a82a6d452aea31cbb678cddf12/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c", size = 5247593, upload-time = "2026-01-02T09:13:17.913Z" }, + { url = "https://files.pythonhosted.org/packages/d2/95/3e0742fe358c4664aed4fd05d5f5373dcdad0b27af52aa0972568541e3f4/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd", size = 6989008, upload-time = "2026-01-02T09:13:20.083Z" }, + { url = "https://files.pythonhosted.org/packages/5a/74/fe2ac378e4e202e56d50540d92e1ef4ff34ed687f3c60f6a121bcf99437e/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc", size = 5313824, upload-time = "2026-01-02T09:13:22.405Z" }, + { url = "https://files.pythonhosted.org/packages/f3/77/2a60dee1adee4e2655ac328dd05c02a955c1cd683b9f1b82ec3feb44727c/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a", size = 5963278, upload-time = "2026-01-02T09:13:24.706Z" }, + { url = "https://files.pythonhosted.org/packages/2d/71/64e9b1c7f04ae0027f788a248e6297d7fcc29571371fe7d45495a78172c0/pillow-12.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19", size = 7029809, upload-time = "2026-01-02T09:13:26.541Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "plumbum" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/c8/11a5f792704b70f071a3dbc329105a98e9cc8d25daaf09f733c44eb0ef8e/plumbum-1.10.0.tar.gz", hash = "sha256:f8cbf0ecec0b73ff4e349398b65112a9e3f9300e7dc019001217dcc148d5c97c", size = 320039, upload-time = "2025-10-31T05:02:48.697Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/ad/45312df6b63ba64ea35b8d8f5f0c577aac16e6b416eafe8e1cb34e03f9a7/plumbum-1.10.0-py3-none-any.whl", hash = "sha256:9583d737ac901c474d99d030e4d5eec4c4e6d2d7417b1cf49728cf3be34f6dc8", size = 127383, upload-time = "2025-10-31T05:02:47.002Z" }, +] + +[[package]] +name = "ply" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/69/882ee5c9d017149285cab114ebeab373308ef0f874fcdac9beb90e0ac4da/ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3", size = 159130, upload-time = "2018-02-15T19:01:31.097Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce", size = 49567, upload-time = "2018-02-15T19:01:27.172Z" }, +] + +[[package]] +name = "prometheus-client" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/58/a794d23feb6b00fc0c72787d7e87d872a6730dd9ed7c7b3e954637d8f280/prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9", size = 85616, upload-time = "2026-01-14T15:26:26.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055", size = 64057, upload-time = "2026-01-14T15:26:24.42Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, +] + +[[package]] +name = "pyarrow" +version = "23.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/33/ffd9c3eb087fa41dd79c3cf20c4c0ae3cdb877c4f8e1107a446006344924/pyarrow-23.0.0.tar.gz", hash = "sha256:180e3150e7edfcd182d3d9afba72f7cf19839a497cc76555a8dce998a8f67615", size = 1167185, upload-time = "2026-01-18T16:19:42.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/c0/57fe251102ca834fee0ef69a84ad33cc0ff9d5dfc50f50b466846356ecd7/pyarrow-23.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:5574d541923efcbfdf1294a2746ae3b8c2498a2dc6cd477882f6f4e7b1ac08d3", size = 34276762, upload-time = "2026-01-18T16:14:34.128Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4e/24130286548a5bc250cbed0b6bbf289a2775378a6e0e6f086ae8c68fc098/pyarrow-23.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:2ef0075c2488932e9d3c2eb3482f9459c4be629aa673b725d5e3cf18f777f8e4", size = 35821420, upload-time = "2026-01-18T16:14:40.699Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/a869e8529d487aa2e842d6c8865eb1e2c9ec33ce2786eb91104d2c3e3f10/pyarrow-23.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:65666fc269669af1ef1c14478c52222a2aa5c907f28b68fb50a203c777e4f60c", size = 44457412, upload-time = "2026-01-18T16:14:49.051Z" }, + { url = "https://files.pythonhosted.org/packages/36/81/1de4f0edfa9a483bbdf0082a05790bd6a20ed2169ea12a65039753be3a01/pyarrow-23.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:4d85cb6177198f3812db4788e394b757223f60d9a9f5ad6634b3e32be1525803", size = 47534285, upload-time = "2026-01-18T16:14:56.748Z" }, + { url = "https://files.pythonhosted.org/packages/f2/04/464a052d673b5ece074518f27377861662449f3c1fdb39ce740d646fd098/pyarrow-23.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1a9ff6fa4141c24a03a1a434c63c8fa97ce70f8f36bccabc18ebba905ddf0f17", size = 48157913, upload-time = "2026-01-18T16:15:05.114Z" }, + { url = "https://files.pythonhosted.org/packages/f4/1b/32a4de9856ee6688c670ca2def588382e573cce45241a965af04c2f61687/pyarrow-23.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:84839d060a54ae734eb60a756aeacb62885244aaa282f3c968f5972ecc7b1ecc", size = 50582529, upload-time = "2026-01-18T16:15:12.846Z" }, + { url = "https://files.pythonhosted.org/packages/db/c7/d6581f03e9b9e44ea60b52d1750ee1a7678c484c06f939f45365a45f7eef/pyarrow-23.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:a149a647dbfe928ce8830a713612aa0b16e22c64feac9d1761529778e4d4eaa5", size = 27542646, upload-time = "2026-01-18T16:15:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bd/c861d020831ee57609b73ea721a617985ece817684dc82415b0bc3e03ac3/pyarrow-23.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5961a9f646c232697c24f54d3419e69b4261ba8a8b66b0ac54a1851faffcbab8", size = 34189116, upload-time = "2026-01-18T16:15:28.054Z" }, + { url = "https://files.pythonhosted.org/packages/8c/23/7725ad6cdcbaf6346221391e7b3eecd113684c805b0a95f32014e6fa0736/pyarrow-23.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:632b3e7c3d232f41d64e1a4a043fb82d44f8a349f339a1188c6a0dd9d2d47d8a", size = 35803831, upload-time = "2026-01-18T16:15:33.798Z" }, + { url = "https://files.pythonhosted.org/packages/57/06/684a421543455cdc2944d6a0c2cc3425b028a4c6b90e34b35580c4899743/pyarrow-23.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:76242c846db1411f1d6c2cc3823be6b86b40567ee24493344f8226ba34a81333", size = 44436452, upload-time = "2026-01-18T16:15:41.598Z" }, + { url = "https://files.pythonhosted.org/packages/c6/6f/8f9eb40c2328d66e8b097777ddcf38494115ff9f1b5bc9754ba46991191e/pyarrow-23.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b73519f8b52ae28127000986bf228fda781e81d3095cd2d3ece76eb5cf760e1b", size = 47557396, upload-time = "2026-01-18T16:15:51.252Z" }, + { url = "https://files.pythonhosted.org/packages/10/6e/f08075f1472e5159553501fde2cc7bc6700944bdabe49a03f8a035ee6ccd/pyarrow-23.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:068701f6823449b1b6469120f399a1239766b117d211c5d2519d4ed5861f75de", size = 48147129, upload-time = "2026-01-18T16:16:00.299Z" }, + { url = "https://files.pythonhosted.org/packages/7d/82/d5a680cd507deed62d141cc7f07f7944a6766fc51019f7f118e4d8ad0fb8/pyarrow-23.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1801ba947015d10e23bca9dd6ef5d0e9064a81569a89b6e9a63b59224fd060df", size = 50596642, upload-time = "2026-01-18T16:16:08.502Z" }, + { url = "https://files.pythonhosted.org/packages/a9/26/4f29c61b3dce9fa7780303b86895ec6a0917c9af927101daaaf118fbe462/pyarrow-23.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:52265266201ec25b6839bf6bd4ea918ca6d50f31d13e1cf200b4261cd11dc25c", size = 27660628, upload-time = "2026-01-18T16:16:15.28Z" }, + { url = "https://files.pythonhosted.org/packages/66/34/564db447d083ec7ff93e0a883a597d2f214e552823bfc178a2d0b1f2c257/pyarrow-23.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:ad96a597547af7827342ffb3c503c8316e5043bb09b47a84885ce39394c96e00", size = 34184630, upload-time = "2026-01-18T16:16:22.141Z" }, + { url = "https://files.pythonhosted.org/packages/aa/3a/3999daebcb5e6119690c92a621c4d78eef2ffba7a0a1b56386d2875fcd77/pyarrow-23.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:b9edf990df77c2901e79608f08c13fbde60202334a4fcadb15c1f57bf7afee43", size = 35796820, upload-time = "2026-01-18T16:16:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ee/39195233056c6a8d0976d7d1ac1cd4fe21fb0ec534eca76bc23ef3f60e11/pyarrow-23.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:36d1b5bc6ddcaff0083ceec7e2561ed61a51f49cce8be079ee8ed406acb6fdef", size = 44438735, upload-time = "2026-01-18T16:16:38.79Z" }, + { url = "https://files.pythonhosted.org/packages/2c/41/6a7328ee493527e7afc0c88d105ecca69a3580e29f2faaeac29308369fd7/pyarrow-23.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4292b889cd224f403304ddda8b63a36e60f92911f89927ec8d98021845ea21be", size = 47557263, upload-time = "2026-01-18T16:16:46.248Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ee/34e95b21ee84db494eae60083ddb4383477b31fb1fd19fd866d794881696/pyarrow-23.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dfd9e133e60eaa847fd80530a1b89a052f09f695d0b9c34c235ea6b2e0924cf7", size = 48153529, upload-time = "2026-01-18T16:16:53.412Z" }, + { url = "https://files.pythonhosted.org/packages/52/88/8a8d83cea30f4563efa1b7bf51d241331ee5cd1b185a7e063f5634eca415/pyarrow-23.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832141cc09fac6aab1cd3719951d23301396968de87080c57c9a7634e0ecd068", size = 50598851, upload-time = "2026-01-18T16:17:01.133Z" }, + { url = "https://files.pythonhosted.org/packages/c6/4c/2929c4be88723ba025e7b3453047dc67e491c9422965c141d24bab6b5962/pyarrow-23.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:7a7d067c9a88faca655c71bcc30ee2782038d59c802d57950826a07f60d83c4c", size = 27577747, upload-time = "2026-01-18T16:18:02.413Z" }, + { url = "https://files.pythonhosted.org/packages/64/52/564a61b0b82d72bd68ec3aef1adda1e3eba776f89134b9ebcb5af4b13cb6/pyarrow-23.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:ce9486e0535a843cf85d990e2ec5820a47918235183a5c7b8b97ed7e92c2d47d", size = 34446038, upload-time = "2026-01-18T16:17:07.861Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c9/232d4f9855fd1de0067c8a7808a363230d223c83aeee75e0fe6eab851ba9/pyarrow-23.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:075c29aeaa685fd1182992a9ed2499c66f084ee54eea47da3eb76e125e06064c", size = 35921142, upload-time = "2026-01-18T16:17:15.401Z" }, + { url = "https://files.pythonhosted.org/packages/96/f2/60af606a3748367b906bb82d41f0032e059f075444445d47e32a7ff1df62/pyarrow-23.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:799965a5379589510d888be3094c2296efd186a17ca1cef5b77703d4d5121f53", size = 44490374, upload-time = "2026-01-18T16:17:23.93Z" }, + { url = "https://files.pythonhosted.org/packages/ff/2d/7731543050a678ea3a413955a2d5d80d2a642f270aa57a3cb7d5a86e3f46/pyarrow-23.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ef7cac8fe6fccd8b9e7617bfac785b0371a7fe26af59463074e4882747145d40", size = 47527896, upload-time = "2026-01-18T16:17:33.393Z" }, + { url = "https://files.pythonhosted.org/packages/5a/90/f3342553b7ac9879413aed46500f1637296f3c8222107523a43a1c08b42a/pyarrow-23.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15a414f710dc927132dd67c361f78c194447479555af57317066ee5116b90e9e", size = 48210401, upload-time = "2026-01-18T16:17:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/f3/da/9862ade205ecc46c172b6ce5038a74b5151c7401e36255f15975a45878b2/pyarrow-23.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e0d2e6915eca7d786be6a77bf227fbc06d825a75b5b5fe9bcbef121dec32685", size = 50579677, upload-time = "2026-01-18T16:17:50.241Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4c/f11f371f5d4740a5dafc2e11c76bcf42d03dfdb2d68696da97de420b6963/pyarrow-23.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:4b317ea6e800b5704e5e5929acb6e2dc13e9276b708ea97a39eb8b345aa2658b", size = 27631889, upload-time = "2026-01-18T16:17:56.55Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/15aec78bcf43a0c004067bd33eb5352836a29a49db8581fc56f2b6ca88b7/pyarrow-23.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:20b187ed9550d233a872074159f765f52f9d92973191cd4b93f293a19efbe377", size = 34213265, upload-time = "2026-01-18T16:18:07.904Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/deb2c594bbba41c37c5d9aa82f510376998352aa69dfcb886cb4b18ad80f/pyarrow-23.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:18ec84e839b493c3886b9b5e06861962ab4adfaeb79b81c76afbd8d84c7d5fda", size = 35819211, upload-time = "2026-01-18T16:18:13.94Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e5/ee82af693cb7b5b2b74f6524cdfede0e6ace779d7720ebca24d68b57c36b/pyarrow-23.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:e438dd3f33894e34fd02b26bd12a32d30d006f5852315f611aa4add6c7fab4bc", size = 44502313, upload-time = "2026-01-18T16:18:20.367Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/95c61ad82236495f3c31987e85135926ba3ec7f3819296b70a68d8066b49/pyarrow-23.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:a244279f240c81f135631be91146d7fa0e9e840e1dfed2aba8483eba25cd98e6", size = 47585886, upload-time = "2026-01-18T16:18:27.544Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6e/a72d901f305201802f016d015de1e05def7706fff68a1dedefef5dc7eff7/pyarrow-23.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c4692e83e42438dba512a570c6eaa42be2f8b6c0f492aea27dec54bdc495103a", size = 48207055, upload-time = "2026-01-18T16:18:35.425Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e5/5de029c537630ca18828db45c30e2a78da03675a70ac6c3528203c416fe3/pyarrow-23.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae7f30f898dfe44ea69654a35c93e8da4cef6606dc4c72394068fd95f8e9f54a", size = 50619812, upload-time = "2026-01-18T16:18:43.553Z" }, + { url = "https://files.pythonhosted.org/packages/59/8d/2af846cd2412e67a087f5bda4a8e23dfd4ebd570f777db2e8686615dafc1/pyarrow-23.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:5b86bb649e4112fb0614294b7d0a175c7513738876b89655605ebb87c804f861", size = 28263851, upload-time = "2026-01-18T16:19:38.567Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7f/caab863e587041156f6786c52e64151b7386742c8c27140f637176e9230e/pyarrow-23.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:ebc017d765d71d80a3f8584ca0566b53e40464586585ac64176115baa0ada7d3", size = 34463240, upload-time = "2026-01-18T16:18:49.755Z" }, + { url = "https://files.pythonhosted.org/packages/c9/fa/3a5b8c86c958e83622b40865e11af0857c48ec763c11d472c87cd518283d/pyarrow-23.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:0800cc58a6d17d159df823f87ad66cefebf105b982493d4bad03ee7fab84b993", size = 35935712, upload-time = "2026-01-18T16:18:55.626Z" }, + { url = "https://files.pythonhosted.org/packages/c5/08/17a62078fc1a53decb34a9aa79cf9009efc74d63d2422e5ade9fed2f99e3/pyarrow-23.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:3a7c68c722da9bb5b0f8c10e3eae71d9825a4b429b40b32709df5d1fa55beb3d", size = 44503523, upload-time = "2026-01-18T16:19:03.958Z" }, + { url = "https://files.pythonhosted.org/packages/cc/70/84d45c74341e798aae0323d33b7c39194e23b1abc439ceaf60a68a7a969a/pyarrow-23.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:bd5556c24622df90551063ea41f559b714aa63ca953db884cfb958559087a14e", size = 47542490, upload-time = "2026-01-18T16:19:11.208Z" }, + { url = "https://files.pythonhosted.org/packages/61/d9/d1274b0e6f19e235de17441e53224f4716574b2ca837022d55702f24d71d/pyarrow-23.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54810f6e6afc4ffee7c2e0051b61722fbea9a4961b46192dcfae8ea12fa09059", size = 48233605, upload-time = "2026-01-18T16:19:19.544Z" }, + { url = "https://files.pythonhosted.org/packages/39/07/e4e2d568cb57543d84482f61e510732820cddb0f47c4bb7df629abfed852/pyarrow-23.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:14de7d48052cf4b0ed174533eafa3cfe0711b8076ad70bede32cf59f744f0d7c", size = 50603979, upload-time = "2026-01-18T16:19:26.717Z" }, + { url = "https://files.pythonhosted.org/packages/72/9c/47693463894b610f8439b2e970b82ef81e9599c757bf2049365e40ff963c/pyarrow-23.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:427deac1f535830a744a4f04a6ac183a64fcac4341b3f618e693c41b7b98d2b0", size = 28338905, upload-time = "2026-01-18T16:19:32.93Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydata-sphinx-theme" +version = "0.16.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "accessible-pygments" }, + { name = "babel" }, + { name = "beautifulsoup4" }, + { name = "docutils" }, + { name = "pygments" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/20/bb50f9de3a6de69e6abd6b087b52fa2418a0418b19597601605f855ad044/pydata_sphinx_theme-0.16.1.tar.gz", hash = "sha256:a08b7f0b7f70387219dc659bff0893a7554d5eb39b59d3b8ef37b8401b7642d7", size = 2412693, upload-time = "2024-12-17T10:53:39.537Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/0d/8ba33fa83a7dcde13eb3c1c2a0c1cc29950a048bfed6d9b0d8b6bd710b4c/pydata_sphinx_theme-0.16.1-py3-none-any.whl", hash = "sha256:225331e8ac4b32682c18fcac5a57a6f717c4e632cea5dd0e247b55155faeccde", size = 6723264, upload-time = "2024-12-17T10:53:35.645Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-json-logger" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/bf/eca6a3d43db1dae7070f70e160ab20b807627ba953663ba07928cdd3dc58/python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f", size = 17683, upload-time = "2025-10-06T04:15:18.984Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548, upload-time = "2025-10-06T04:15:17.553Z" }, +] + +[[package]] +name = "pyvisa" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/04/1833acfa4ddefc5cdcfd76607f1bf494e7b7b658d890626e23026655d599/pyvisa-1.15.0.tar.gz", hash = "sha256:cec3cb91703a2849f6faa42b1ecd7689a0175baabff8ca33fce9f45934ce45e6", size = 236289, upload-time = "2025-04-01T15:52:25.377Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/12/4979772f36acceb57664bde282fc7bd3e67f8d0ce85f2a521a05e90baaa5/pyvisa-1.15.0-py3-none-any.whl", hash = "sha256:e3ac8d9e863fbdbbe7e6d4d91401bceb7914d1c4a558a89b2cc755789f1e8309", size = 179199, upload-time = "2025-04-01T15:52:23.631Z" }, +] + +[[package]] +name = "pyviz-comms" +version = "3.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "param" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/ee/2b5367b911bab506662abffe6f342101a9b3edacee91ff9afe62db5fe9a7/pyviz_comms-3.0.6.tar.gz", hash = "sha256:73d66b620390d97959b2c4d8a2c0778d41fe20581be4717f01e46b8fae8c5695", size = 197772, upload-time = "2025-06-20T16:50:30.97Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/5a/f8c0868199bbb231a02616286ce8a4ccb85f5387b9215510297dcfedd214/pyviz_comms-3.0.6-py3-none-any.whl", hash = "sha256:4eba6238cd4a7f4add2d11879ce55411785b7d38a7c5dba42c7a0826ca53e6c2", size = 84275, upload-time = "2025-06-20T16:50:28.826Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "pywinpty" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/54/37c7370ba91f579235049dc26cd2c5e657d2a943e01820844ffc81f32176/pywinpty-3.0.3.tar.gz", hash = "sha256:523441dc34d231fb361b4b00f8c99d3f16de02f5005fd544a0183112bcc22412", size = 31309, upload-time = "2026-02-04T21:51:09.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/c3/3e75075c7f71735f22b66fab0481f2c98e3a4d58cba55cb50ba29114bcf6/pywinpty-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:dff25a9a6435f527d7c65608a7e62783fc12076e7d44487a4911ee91be5a8ac8", size = 2114430, upload-time = "2026-02-04T21:54:19.485Z" }, + { url = "https://files.pythonhosted.org/packages/8d/1e/8a54166a8c5e4f5cb516514bdf4090be4d51a71e8d9f6d98c0aa00fe45d4/pywinpty-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:fbc1e230e5b193eef4431cba3f39996a288f9958f9c9f092c8a961d930ee8f68", size = 236191, upload-time = "2026-02-04T21:50:36.239Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d4/aeb5e1784d2c5bff6e189138a9ca91a090117459cea0c30378e1f2db3d54/pywinpty-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:c9081df0e49ffa86d15db4a6ba61530630e48707f987df42c9d3313537e81fc0", size = 2113098, upload-time = "2026-02-04T21:54:37.711Z" }, + { url = "https://files.pythonhosted.org/packages/b9/53/7278223c493ccfe4883239cf06c823c56460a8010e0fc778eef67858dc14/pywinpty-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:15e79d870e18b678fb8a5a6105fd38496b55697c66e6fc0378236026bc4d59e9", size = 234901, upload-time = "2026-02-04T21:53:31.35Z" }, + { url = "https://files.pythonhosted.org/packages/e5/cb/58d6ed3fd429c96a90ef01ac9a617af10a6d41469219c25e7dc162abbb71/pywinpty-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9c91dbb026050c77bdcef964e63a4f10f01a639113c4d3658332614544c467ab", size = 2112686, upload-time = "2026-02-04T21:52:03.035Z" }, + { url = "https://files.pythonhosted.org/packages/fd/50/724ed5c38c504d4e58a88a072776a1e880d970789deaeb2b9f7bd9a5141a/pywinpty-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:fe1f7911805127c94cf51f89ab14096c6f91ffdcacf993d2da6082b2142a2523", size = 234591, upload-time = "2026-02-04T21:52:29.821Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ad/90a110538696b12b39fd8758a06d70ded899308198ad2305ac68e361126e/pywinpty-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:3f07a6cf1c1d470d284e614733c3d0f726d2c85e78508ea10a403140c3c0c18a", size = 2112360, upload-time = "2026-02-04T21:55:33.397Z" }, + { url = "https://files.pythonhosted.org/packages/44/0f/7ffa221757a220402bc79fda44044c3f2cc57338d878ab7d622add6f4581/pywinpty-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:15c7c0b6f8e9d87aabbaff76468dabf6e6121332c40fc1d83548d02a9d6a3759", size = 233107, upload-time = "2026-02-04T21:51:45.455Z" }, + { url = "https://files.pythonhosted.org/packages/28/88/2ff917caff61e55f38bcdb27de06ee30597881b2cae44fbba7627be015c4/pywinpty-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:d4b6b7b0fe0cdcd02e956bd57cfe9f4e5a06514eecf3b5ae174da4f951b58be9", size = 2113282, upload-time = "2026-02-04T21:52:08.188Z" }, + { url = "https://files.pythonhosted.org/packages/63/32/40a775343ace542cc43ece3f1d1fce454021521ecac41c4c4573081c2336/pywinpty-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:34789d685fc0d547ce0c8a65e5a70e56f77d732fa6e03c8f74fefb8cbb252019", size = 234207, upload-time = "2026-02-04T21:51:58.687Z" }, + { url = "https://files.pythonhosted.org/packages/8d/54/5d5e52f4cb75028104ca6faf36c10f9692389b1986d34471663b4ebebd6d/pywinpty-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:0c37e224a47a971d1a6e08649a1714dac4f63c11920780977829ed5c8cadead1", size = 2112910, upload-time = "2026-02-04T21:52:30.976Z" }, + { url = "https://files.pythonhosted.org/packages/0a/44/dcd184824e21d4620b06c7db9fbb15c3ad0a0f1fa2e6de79969fb82647ec/pywinpty-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c4e9c3dff7d86ba81937438d5819f19f385a39d8f592d4e8af67148ceb4f6ab5", size = 233425, upload-time = "2026-02-04T21:51:56.754Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "pyzmq" +version = "27.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, + { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, + { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, + { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, + { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, + { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, + { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, + { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, + { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, + { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, +] + +[[package]] +name = "qcodes" +version = "0.54.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "broadbean" }, + { name = "cf-xarray" }, + { name = "dask" }, + { name = "h5netcdf" }, + { name = "h5py" }, + { name = "ipykernel" }, + { name = "ipywidgets" }, + { name = "jsonschema" }, + { name = "matplotlib" }, + { name = "networkx" }, + { name = "numpy" }, + { name = "opentelemetry-api" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "pyarrow" }, + { name = "pyvisa" }, + { name = "ruamel-yaml" }, + { name = "tabulate" }, + { name = "tqdm" }, + { name = "typing-extensions" }, + { name = "uncertainties" }, + { name = "versioningit" }, + { name = "websockets" }, + { name = "xarray" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/3b/38c613958911c7ea14d852632042e834749558416eea6b93761802bdf6da/qcodes-0.54.4.tar.gz", hash = "sha256:949cf3907fda1af176f545ee7aa3853119204a4c34beafef3fd569aa46521d9a", size = 817999, upload-time = "2025-12-12T17:39:33.878Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/ee/a08a9240e25b132f1becc86609ccf98f1261a06370aec3ef6458519962e7/qcodes-0.54.4-py3-none-any.whl", hash = "sha256:29df1dfe9b84ae38c03342eb035e7c0523ff3824a51ff45c858cbb04212a7162", size = 977283, upload-time = "2025-12-12T17:39:31.669Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rfc3339-validator" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, +] + +[[package]] +name = "rfc3986-validator" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/88/f270de456dd7d11dcc808abfa291ecdd3f45ff44e3b549ffa01b126464d0/rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055", size = 6760, upload-time = "2019-10-28T16:00:19.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9", size = 4242, upload-time = "2019-10-28T16:00:13.976Z" }, +] + +[[package]] +name = "rfc3987-syntax" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lark" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/06/37c1a5557acf449e8e406a830a05bf885ac47d33270aec454ef78675008d/rfc3987_syntax-1.1.0.tar.gz", hash = "sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d", size = 14239, upload-time = "2025-07-18T01:05:05.015Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", size = 8046, upload-time = "2025-07-18T01:05:03.843Z" }, +] + +[[package]] +name = "roman-numerals" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "ruamel-yaml" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/04/eab13a954e763b0606f460443fcbf6bb5a0faf06890ea3754ff16523dce5/ruff-0.15.2.tar.gz", hash = "sha256:14b965afee0969e68bb871eba625343b8673375f457af4abe98553e8bbb98342", size = 4558148, upload-time = "2026-02-19T22:32:20.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/70/3a4dc6d09b13cb3e695f28307e5d889b2e1a66b7af9c5e257e796695b0e6/ruff-0.15.2-py3-none-linux_armv6l.whl", hash = "sha256:120691a6fdae2f16d65435648160f5b81a9625288f75544dc40637436b5d3c0d", size = 10430565, upload-time = "2026-02-19T22:32:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/71/0b/bb8457b56185ece1305c666dc895832946d24055be90692381c31d57466d/ruff-0.15.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a89056d831256099658b6bba4037ac6dd06f49d194199215befe2bb10457ea5e", size = 10820354, upload-time = "2026-02-19T22:32:07.366Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e36dee3a64be0ebd23c86ffa3aa3fd3ac9a712ff295e192243f814a830b6bd87", size = 10170767, upload-time = "2026-02-19T22:32:13.188Z" }, + { url = "https://files.pythonhosted.org/packages/47/e8/da1aa341d3af017a21c7a62fb5ec31d4e7ad0a93ab80e3a508316efbcb23/ruff-0.15.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9fb47b6d9764677f8c0a193c0943ce9a05d6763523f132325af8a858eadc2b9", size = 10529591, upload-time = "2026-02-19T22:32:02.547Z" }, + { url = "https://files.pythonhosted.org/packages/93/74/184fbf38e9f3510231fbc5e437e808f0b48c42d1df9434b208821efcd8d6/ruff-0.15.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f376990f9d0d6442ea9014b19621d8f2aaf2b8e39fdbfc79220b7f0c596c9b80", size = 10260771, upload-time = "2026-02-19T22:32:36.938Z" }, + { url = "https://files.pythonhosted.org/packages/05/ac/605c20b8e059a0bc4b42360414baa4892ff278cec1c91fff4be0dceedefd/ruff-0.15.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dcc987551952d73cbf5c88d9fdee815618d497e4df86cd4c4824cc59d5dd75f", size = 11045791, upload-time = "2026-02-19T22:32:31.642Z" }, + { url = "https://files.pythonhosted.org/packages/fd/52/db6e419908f45a894924d410ac77d64bdd98ff86901d833364251bd08e22/ruff-0.15.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a47fd785cbe8c01b9ff45031af875d101b040ad8f4de7bbb716487c74c9a77", size = 11879271, upload-time = "2026-02-19T22:32:29.305Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d8/7992b18f2008bdc9231d0f10b16df7dda964dbf639e2b8b4c1b4e91b83af/ruff-0.15.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbe9f49354866e575b4c6943856989f966421870e85cd2ac94dccb0a9dcb2fea", size = 11303707, upload-time = "2026-02-19T22:32:22.492Z" }, + { url = "https://files.pythonhosted.org/packages/d7/02/849b46184bcfdd4b64cde61752cc9a146c54759ed036edd11857e9b8443b/ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7a672c82b5f9887576087d97be5ce439f04bbaf548ee987b92d3a7dede41d3a", size = 11149151, upload-time = "2026-02-19T22:32:44.234Z" }, + { url = "https://files.pythonhosted.org/packages/70/04/f5284e388bab60d1d3b99614a5a9aeb03e0f333847e2429bebd2aaa1feec/ruff-0.15.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ecc64f46f7019e2bcc3cdc05d4a7da958b629a5ab7033195e11a438403d956", size = 11091132, upload-time = "2026-02-19T22:32:24.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ae/88d844a21110e14d92cf73d57363fab59b727ebeabe78009b9ccb23500af/ruff-0.15.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8dcf243b15b561c655c1ef2f2b0050e5d50db37fe90115507f6ff37d865dc8b4", size = 10504717, upload-time = "2026-02-19T22:32:26.75Z" }, + { url = "https://files.pythonhosted.org/packages/64/27/867076a6ada7f2b9c8292884ab44d08fd2ba71bd2b5364d4136f3cd537e1/ruff-0.15.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dab6941c862c05739774677c6273166d2510d254dac0695c0e3f5efa1b5585de", size = 10263122, upload-time = "2026-02-19T22:32:10.036Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ef/faf9321d550f8ebf0c6373696e70d1758e20ccdc3951ad7af00c0956be7c/ruff-0.15.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b9164f57fc36058e9a6806eb92af185b0697c9fe4c7c52caa431c6554521e5c", size = 10735295, upload-time = "2026-02-19T22:32:39.227Z" }, + { url = "https://files.pythonhosted.org/packages/2f/55/e8089fec62e050ba84d71b70e7834b97709ca9b7aba10c1a0b196e493f97/ruff-0.15.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:80d24fcae24d42659db7e335b9e1531697a7102c19185b8dc4a028b952865fd8", size = 11241641, upload-time = "2026-02-19T22:32:34.617Z" }, + { url = "https://files.pythonhosted.org/packages/23/01/1c30526460f4d23222d0fabd5888868262fd0e2b71a00570ca26483cd993/ruff-0.15.2-py3-none-win32.whl", hash = "sha256:fd5ff9e5f519a7e1bd99cbe8daa324010a74f5e2ebc97c6242c08f26f3714f6f", size = 10507885, upload-time = "2026-02-19T22:32:15.635Z" }, + { url = "https://files.pythonhosted.org/packages/5c/10/3d18e3bbdf8fc50bbb4ac3cc45970aa5a9753c5cb51bf9ed9a3cd8b79fa3/ruff-0.15.2-py3-none-win_amd64.whl", hash = "sha256:d20014e3dfa400f3ff84830dfb5755ece2de45ab62ecea4af6b7262d0fb4f7c5", size = 11623725, upload-time = "2026-02-19T22:32:04.947Z" }, + { url = "https://files.pythonhosted.org/packages/6d/78/097c0798b1dab9f8affe73da9642bb4500e098cb27fd8dc9724816ac747b/ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e", size = 10941649, upload-time = "2026-02-19T22:32:18.108Z" }, +] + +[[package]] +name = "schema" +version = "0.7.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/2e/8da627b65577a8f130fe9dfa88ce94fcb24b1f8b59e0fc763ee61abef8b8/schema-0.7.8.tar.gz", hash = "sha256:e86cc08edd6fe6e2522648f4e47e3a31920a76e82cce8937535422e310862ab5", size = 45540, upload-time = "2025-10-11T13:15:40.281Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/75/aad85817266ac5285c93391711d231ca63e9ae7d42cd3ca37549e24ebe52/schema-0.7.8-py2.py3-none-any.whl", hash = "sha256:00bd977fadc7d9521bf289850cd8a8aa5f4948f575476b8daaa5c1b57af2dce1", size = 19108, upload-time = "2025-10-11T17:13:07.323Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/3e/9cca699f3486ce6bc12ff46dc2031f1ec8eb9ccc9a320fdaf925f1417426/scipy-1.17.0.tar.gz", hash = "sha256:2591060c8e648d8b96439e111ac41fd8342fdeff1876be2e19dea3fe8930454e", size = 30396830, upload-time = "2026-01-10T21:34:23.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/4b/c89c131aa87cad2b77a54eb0fb94d633a842420fa7e919dc2f922037c3d8/scipy-1.17.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:2abd71643797bd8a106dff97894ff7869eeeb0af0f7a5ce02e4227c6a2e9d6fd", size = 31381316, upload-time = "2026-01-10T21:24:33.42Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5f/a6b38f79a07d74989224d5f11b55267714707582908a5f1ae854cf9a9b84/scipy-1.17.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:ef28d815f4d2686503e5f4f00edc387ae58dfd7a2f42e348bb53359538f01558", size = 27966760, upload-time = "2026-01-10T21:24:38.911Z" }, + { url = "https://files.pythonhosted.org/packages/c1/20/095ad24e031ee8ed3c5975954d816b8e7e2abd731e04f8be573de8740885/scipy-1.17.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:272a9f16d6bb4667e8b50d25d71eddcc2158a214df1b566319298de0939d2ab7", size = 20138701, upload-time = "2026-01-10T21:24:43.249Z" }, + { url = "https://files.pythonhosted.org/packages/89/11/4aad2b3858d0337756f3323f8960755704e530b27eb2a94386c970c32cbe/scipy-1.17.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:7204fddcbec2fe6598f1c5fdf027e9f259106d05202a959a9f1aecf036adc9f6", size = 22480574, upload-time = "2026-01-10T21:24:47.266Z" }, + { url = "https://files.pythonhosted.org/packages/85/bd/f5af70c28c6da2227e510875cadf64879855193a687fb19951f0f44cfd6b/scipy-1.17.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc02c37a5639ee67d8fb646ffded6d793c06c5622d36b35cfa8fe5ececb8f042", size = 32862414, upload-time = "2026-01-10T21:24:52.566Z" }, + { url = "https://files.pythonhosted.org/packages/ef/df/df1457c4df3826e908879fe3d76bc5b6e60aae45f4ee42539512438cfd5d/scipy-1.17.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dac97a27520d66c12a34fd90a4fe65f43766c18c0d6e1c0a80f114d2260080e4", size = 35112380, upload-time = "2026-01-10T21:24:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/5f/bb/88e2c16bd1dd4de19d80d7c5e238387182993c2fb13b4b8111e3927ad422/scipy-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb7446a39b3ae0fe8f416a9a3fdc6fba3f11c634f680f16a239c5187bc487c0", size = 34922676, upload-time = "2026-01-10T21:25:04.287Z" }, + { url = "https://files.pythonhosted.org/packages/02/ba/5120242cc735f71fc002cff0303d536af4405eb265f7c60742851e7ccfe9/scipy-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:474da16199f6af66601a01546144922ce402cb17362e07d82f5a6cf8f963e449", size = 37507599, upload-time = "2026-01-10T21:25:09.851Z" }, + { url = "https://files.pythonhosted.org/packages/52/c8/08629657ac6c0da198487ce8cd3de78e02cfde42b7f34117d56a3fe249dc/scipy-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:255c0da161bd7b32a6c898e7891509e8a9289f0b1c6c7d96142ee0d2b114c2ea", size = 36380284, upload-time = "2026-01-10T21:25:15.632Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4a/465f96d42c6f33ad324a40049dfd63269891db9324aa66c4a1c108c6f994/scipy-1.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:85b0ac3ad17fa3be50abd7e69d583d98792d7edc08367e01445a1e2076005379", size = 24370427, upload-time = "2026-01-10T21:25:20.514Z" }, + { url = "https://files.pythonhosted.org/packages/0b/11/7241a63e73ba5a516f1930ac8d5b44cbbfabd35ac73a2d08ca206df007c4/scipy-1.17.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:0d5018a57c24cb1dd828bcf51d7b10e65986d549f52ef5adb6b4d1ded3e32a57", size = 31364580, upload-time = "2026-01-10T21:25:25.717Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1d/5057f812d4f6adc91a20a2d6f2ebcdb517fdbc87ae3acc5633c9b97c8ba5/scipy-1.17.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:88c22af9e5d5a4f9e027e26772cc7b5922fab8bcc839edb3ae33de404feebd9e", size = 27969012, upload-time = "2026-01-10T21:25:30.921Z" }, + { url = "https://files.pythonhosted.org/packages/e3/21/f6ec556c1e3b6ec4e088da667d9987bb77cc3ab3026511f427dc8451187d/scipy-1.17.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f3cd947f20fe17013d401b64e857c6b2da83cae567adbb75b9dcba865abc66d8", size = 20140691, upload-time = "2026-01-10T21:25:34.802Z" }, + { url = "https://files.pythonhosted.org/packages/7a/fe/5e5ad04784964ba964a96f16c8d4676aa1b51357199014dce58ab7ec5670/scipy-1.17.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e8c0b331c2c1f531eb51f1b4fc9ba709521a712cce58f1aa627bc007421a5306", size = 22463015, upload-time = "2026-01-10T21:25:39.277Z" }, + { url = "https://files.pythonhosted.org/packages/4a/69/7c347e857224fcaf32a34a05183b9d8a7aca25f8f2d10b8a698b8388561a/scipy-1.17.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5194c445d0a1c7a6c1a4a4681b6b7c71baad98ff66d96b949097e7513c9d6742", size = 32724197, upload-time = "2026-01-10T21:25:44.084Z" }, + { url = "https://files.pythonhosted.org/packages/d1/fe/66d73b76d378ba8cc2fe605920c0c75092e3a65ae746e1e767d9d020a75a/scipy-1.17.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9eeb9b5f5997f75507814ed9d298ab23f62cf79f5a3ef90031b1ee2506abdb5b", size = 35009148, upload-time = "2026-01-10T21:25:50.591Z" }, + { url = "https://files.pythonhosted.org/packages/af/07/07dec27d9dc41c18d8c43c69e9e413431d20c53a0339c388bcf72f353c4b/scipy-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:40052543f7bbe921df4408f46003d6f01c6af109b9e2c8a66dd1cf6cf57f7d5d", size = 34798766, upload-time = "2026-01-10T21:25:59.41Z" }, + { url = "https://files.pythonhosted.org/packages/81/61/0470810c8a093cdacd4ba7504b8a218fd49ca070d79eca23a615f5d9a0b0/scipy-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0cf46c8013fec9d3694dc572f0b54100c28405d55d3e2cb15e2895b25057996e", size = 37405953, upload-time = "2026-01-10T21:26:07.75Z" }, + { url = "https://files.pythonhosted.org/packages/92/ce/672ed546f96d5d41ae78c4b9b02006cedd0b3d6f2bf5bb76ea455c320c28/scipy-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:0937a0b0d8d593a198cededd4c439a0ea216a3f36653901ea1f3e4be949056f8", size = 36328121, upload-time = "2026-01-10T21:26:16.509Z" }, + { url = "https://files.pythonhosted.org/packages/9d/21/38165845392cae67b61843a52c6455d47d0cc2a40dd495c89f4362944654/scipy-1.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:f603d8a5518c7426414d1d8f82e253e454471de682ce5e39c29adb0df1efb86b", size = 24314368, upload-time = "2026-01-10T21:26:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/0c/51/3468fdfd49387ddefee1636f5cf6d03ce603b75205bf439bbf0e62069bfd/scipy-1.17.0-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:65ec32f3d32dfc48c72df4291345dae4f048749bc8d5203ee0a3f347f96c5ce6", size = 31344101, upload-time = "2026-01-10T21:26:30.25Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9a/9406aec58268d437636069419e6977af953d1e246df941d42d3720b7277b/scipy-1.17.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:1f9586a58039d7229ce77b52f8472c972448cded5736eaf102d5658bbac4c269", size = 27950385, upload-time = "2026-01-10T21:26:36.801Z" }, + { url = "https://files.pythonhosted.org/packages/4f/98/e7342709e17afdfd1b26b56ae499ef4939b45a23a00e471dfb5375eea205/scipy-1.17.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9fad7d3578c877d606b1150135c2639e9de9cecd3705caa37b66862977cc3e72", size = 20122115, upload-time = "2026-01-10T21:26:42.107Z" }, + { url = "https://files.pythonhosted.org/packages/fd/0e/9eeeb5357a64fd157cbe0302c213517c541cc16b8486d82de251f3c68ede/scipy-1.17.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:423ca1f6584fc03936972b5f7c06961670dbba9f234e71676a7c7ccf938a0d61", size = 22442402, upload-time = "2026-01-10T21:26:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c9/10/be13397a0e434f98e0c79552b2b584ae5bb1c8b2be95db421533bbca5369/scipy-1.17.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe508b5690e9eaaa9467fc047f833af58f1152ae51a0d0aed67aa5801f4dd7d6", size = 32696338, upload-time = "2026-01-10T21:26:55.521Z" }, + { url = "https://files.pythonhosted.org/packages/63/1e/12fbf2a3bb240161651c94bb5cdd0eae5d4e8cc6eaeceb74ab07b12a753d/scipy-1.17.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6680f2dfd4f6182e7d6db161344537da644d1cf85cf293f015c60a17ecf08752", size = 34977201, upload-time = "2026-01-10T21:27:03.501Z" }, + { url = "https://files.pythonhosted.org/packages/19/5b/1a63923e23ccd20bd32156d7dd708af5bbde410daa993aa2500c847ab2d2/scipy-1.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eec3842ec9ac9de5917899b277428886042a93db0b227ebbe3a333b64ec7643d", size = 34777384, upload-time = "2026-01-10T21:27:11.423Z" }, + { url = "https://files.pythonhosted.org/packages/39/22/b5da95d74edcf81e540e467202a988c50fef41bd2011f46e05f72ba07df6/scipy-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d7425fcafbc09a03731e1bc05581f5fad988e48c6a861f441b7ab729a49a55ea", size = 37379586, upload-time = "2026-01-10T21:27:20.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b6/8ac583d6da79e7b9e520579f03007cb006f063642afd6b2eeb16b890bf93/scipy-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:87b411e42b425b84777718cc41516b8a7e0795abfa8e8e1d573bf0ef014f0812", size = 36287211, upload-time = "2026-01-10T21:28:43.122Z" }, + { url = "https://files.pythonhosted.org/packages/55/fb/7db19e0b3e52f882b420417644ec81dd57eeef1bd1705b6f689d8ff93541/scipy-1.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:357ca001c6e37601066092e7c89cca2f1ce74e2a520ca78d063a6d2201101df2", size = 24312646, upload-time = "2026-01-10T21:28:49.893Z" }, + { url = "https://files.pythonhosted.org/packages/20/b6/7feaa252c21cc7aff335c6c55e1b90ab3e3306da3f048109b8b639b94648/scipy-1.17.0-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:ec0827aa4d36cb79ff1b81de898e948a51ac0b9b1c43e4a372c0508c38c0f9a3", size = 31693194, upload-time = "2026-01-10T21:27:27.454Z" }, + { url = "https://files.pythonhosted.org/packages/76/bb/bbb392005abce039fb7e672cb78ac7d158700e826b0515cab6b5b60c26fb/scipy-1.17.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:819fc26862b4b3c73a60d486dbb919202f3d6d98c87cf20c223511429f2d1a97", size = 28365415, upload-time = "2026-01-10T21:27:34.26Z" }, + { url = "https://files.pythonhosted.org/packages/37/da/9d33196ecc99fba16a409c691ed464a3a283ac454a34a13a3a57c0d66f3a/scipy-1.17.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:363ad4ae2853d88ebcde3ae6ec46ccca903ea9835ee8ba543f12f575e7b07e4e", size = 20537232, upload-time = "2026-01-10T21:27:40.306Z" }, + { url = "https://files.pythonhosted.org/packages/56/9d/f4b184f6ddb28e9a5caea36a6f98e8ecd2a524f9127354087ce780885d83/scipy-1.17.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:979c3a0ff8e5ba254d45d59ebd38cde48fce4f10b5125c680c7a4bfe177aab07", size = 22791051, upload-time = "2026-01-10T21:27:46.539Z" }, + { url = "https://files.pythonhosted.org/packages/9b/9d/025cccdd738a72140efc582b1641d0dd4caf2e86c3fb127568dc80444e6e/scipy-1.17.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:130d12926ae34399d157de777472bf82e9061c60cc081372b3118edacafe1d00", size = 32815098, upload-time = "2026-01-10T21:27:54.389Z" }, + { url = "https://files.pythonhosted.org/packages/48/5f/09b879619f8bca15ce392bfc1894bd9c54377e01d1b3f2f3b595a1b4d945/scipy-1.17.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e886000eb4919eae3a44f035e63f0fd8b651234117e8f6f29bad1cd26e7bc45", size = 35031342, upload-time = "2026-01-10T21:28:03.012Z" }, + { url = "https://files.pythonhosted.org/packages/f2/9a/f0f0a9f0aa079d2f106555b984ff0fbb11a837df280f04f71f056ea9c6e4/scipy-1.17.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:13c4096ac6bc31d706018f06a49abe0485f96499deb82066b94d19b02f664209", size = 34893199, upload-time = "2026-01-10T21:28:10.832Z" }, + { url = "https://files.pythonhosted.org/packages/90/b8/4f0f5cf0c5ea4d7548424e6533e6b17d164f34a6e2fb2e43ffebb6697b06/scipy-1.17.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cacbaddd91fcffde703934897c5cd2c7cb0371fac195d383f4e1f1c5d3f3bd04", size = 37438061, upload-time = "2026-01-10T21:28:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/f9/cc/2bd59140ed3b2fa2882fb15da0a9cb1b5a6443d67cfd0d98d4cec83a57ec/scipy-1.17.0-cp313-cp313t-win_amd64.whl", hash = "sha256:edce1a1cf66298cccdc48a1bdf8fb10a3bf58e8b58d6c3883dd1530e103f87c0", size = 36328593, upload-time = "2026-01-10T21:28:28.007Z" }, + { url = "https://files.pythonhosted.org/packages/13/1b/c87cc44a0d2c7aaf0f003aef2904c3d097b422a96c7e7c07f5efd9073c1b/scipy-1.17.0-cp313-cp313t-win_arm64.whl", hash = "sha256:30509da9dbec1c2ed8f168b8d8aa853bc6723fede1dbc23c7d43a56f5ab72a67", size = 24625083, upload-time = "2026-01-10T21:28:35.188Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2d/51006cd369b8e7879e1c630999a19d1fbf6f8b5ed3e33374f29dc87e53b3/scipy-1.17.0-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:c17514d11b78be8f7e6331b983a65a7f5ca1fd037b95e27b280921fe5606286a", size = 31346803, upload-time = "2026-01-10T21:28:57.24Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2e/2349458c3ce445f53a6c93d4386b1c4c5c0c540917304c01222ff95ff317/scipy-1.17.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:4e00562e519c09da34c31685f6acc3aa384d4d50604db0f245c14e1b4488bfa2", size = 27967182, upload-time = "2026-01-10T21:29:04.107Z" }, + { url = "https://files.pythonhosted.org/packages/5e/7c/df525fbfa77b878d1cfe625249529514dc02f4fd5f45f0f6295676a76528/scipy-1.17.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f7df7941d71314e60a481e02d5ebcb3f0185b8d799c70d03d8258f6c80f3d467", size = 20139125, upload-time = "2026-01-10T21:29:10.179Z" }, + { url = "https://files.pythonhosted.org/packages/33/11/fcf9d43a7ed1234d31765ec643b0515a85a30b58eddccc5d5a4d12b5f194/scipy-1.17.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:aabf057c632798832f071a8dde013c2e26284043934f53b00489f1773b33527e", size = 22443554, upload-time = "2026-01-10T21:29:15.888Z" }, + { url = "https://files.pythonhosted.org/packages/80/5c/ea5d239cda2dd3d31399424967a24d556cf409fbea7b5b21412b0fd0a44f/scipy-1.17.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a38c3337e00be6fd8a95b4ed66b5d988bac4ec888fd922c2ea9fe5fb1603dd67", size = 32757834, upload-time = "2026-01-10T21:29:23.406Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7e/8c917cc573310e5dc91cbeead76f1b600d3fb17cf0969db02c9cf92e3cfa/scipy-1.17.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00fb5f8ec8398ad90215008d8b6009c9db9fa924fd4c7d6be307c6f945f9cd73", size = 34995775, upload-time = "2026-01-10T21:29:31.915Z" }, + { url = "https://files.pythonhosted.org/packages/c5/43/176c0c3c07b3f7df324e7cdd933d3e2c4898ca202b090bd5ba122f9fe270/scipy-1.17.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f2a4942b0f5f7c23c7cd641a0ca1955e2ae83dedcff537e3a0259096635e186b", size = 34841240, upload-time = "2026-01-10T21:29:39.995Z" }, + { url = "https://files.pythonhosted.org/packages/44/8c/d1f5f4b491160592e7f084d997de53a8e896a3ac01cd07e59f43ca222744/scipy-1.17.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf133ced83889583156566d2bdf7a07ff89228fe0c0cb727f777de92092ec6b", size = 37394463, upload-time = "2026-01-10T21:29:48.723Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ec/42a6657f8d2d087e750e9a5dde0b481fd135657f09eaf1cf5688bb23c338/scipy-1.17.0-cp314-cp314-win_amd64.whl", hash = "sha256:3625c631a7acd7cfd929e4e31d2582cf00f42fcf06011f59281271746d77e061", size = 37053015, upload-time = "2026-01-10T21:30:51.418Z" }, + { url = "https://files.pythonhosted.org/packages/27/58/6b89a6afd132787d89a362d443a7bddd511b8f41336a1ae47f9e4f000dc4/scipy-1.17.0-cp314-cp314-win_arm64.whl", hash = "sha256:9244608d27eafe02b20558523ba57f15c689357c85bdcfe920b1828750aa26eb", size = 24951312, upload-time = "2026-01-10T21:30:56.771Z" }, + { url = "https://files.pythonhosted.org/packages/e9/01/f58916b9d9ae0112b86d7c3b10b9e685625ce6e8248df139d0fcb17f7397/scipy-1.17.0-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:2b531f57e09c946f56ad0b4a3b2abee778789097871fc541e267d2eca081cff1", size = 31706502, upload-time = "2026-01-10T21:29:56.326Z" }, + { url = "https://files.pythonhosted.org/packages/59/8e/2912a87f94a7d1f8b38aabc0faf74b82d3b6c9e22be991c49979f0eceed8/scipy-1.17.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:13e861634a2c480bd237deb69333ac79ea1941b94568d4b0efa5db5e263d4fd1", size = 28380854, upload-time = "2026-01-10T21:30:01.554Z" }, + { url = "https://files.pythonhosted.org/packages/bd/1c/874137a52dddab7d5d595c1887089a2125d27d0601fce8c0026a24a92a0b/scipy-1.17.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:eb2651271135154aa24f6481cbae5cc8af1f0dd46e6533fb7b56aa9727b6a232", size = 20552752, upload-time = "2026-01-10T21:30:05.93Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f0/7518d171cb735f6400f4576cf70f756d5b419a07fe1867da34e2c2c9c11b/scipy-1.17.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:c5e8647f60679790c2f5c76be17e2e9247dc6b98ad0d3b065861e082c56e078d", size = 22803972, upload-time = "2026-01-10T21:30:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/7c/74/3498563a2c619e8a3ebb4d75457486c249b19b5b04a30600dfd9af06bea5/scipy-1.17.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fb10d17e649e1446410895639f3385fd2bf4c3c7dfc9bea937bddcbc3d7b9ba", size = 32829770, upload-time = "2026-01-10T21:30:16.359Z" }, + { url = "https://files.pythonhosted.org/packages/48/d1/7b50cedd8c6c9d6f706b4b36fa8544d829c712a75e370f763b318e9638c1/scipy-1.17.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8547e7c57f932e7354a2319fab613981cde910631979f74c9b542bb167a8b9db", size = 35051093, upload-time = "2026-01-10T21:30:22.987Z" }, + { url = "https://files.pythonhosted.org/packages/e2/82/a2d684dfddb87ba1b3ea325df7c3293496ee9accb3a19abe9429bce94755/scipy-1.17.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33af70d040e8af9d5e7a38b5ed3b772adddd281e3062ff23fec49e49681c38cf", size = 34909905, upload-time = "2026-01-10T21:30:28.704Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5e/e565bd73991d42023eb82bb99e51c5b3d9e2c588ca9d4b3e2cc1d3ca62a6/scipy-1.17.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb55bb97d00f8b7ab95cb64f873eb0bf54d9446264d9f3609130381233483f", size = 37457743, upload-time = "2026-01-10T21:30:34.819Z" }, + { url = "https://files.pythonhosted.org/packages/58/a8/a66a75c3d8f1fb2b83f66007d6455a06a6f6cf5618c3dc35bc9b69dd096e/scipy-1.17.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1ff269abf702f6c7e67a4b7aad981d42871a11b9dd83c58d2d2ea624efbd1088", size = 37098574, upload-time = "2026-01-10T21:30:40.782Z" }, + { url = "https://files.pythonhosted.org/packages/56/a5/df8f46ef7da168f1bc52cd86e09a9de5c6f19cc1da04454d51b7d4f43408/scipy-1.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:031121914e295d9791319a1875444d55079885bbae5bdc9c5e0f2ee5f09d34ff", size = 25246266, upload-time = "2026-01-10T21:30:45.923Z" }, +] + +[[package]] +name = "seaborn" +version = "0.13.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib" }, + { name = "numpy" }, + { name = "pandas" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/59/a451d7420a77ab0b98f7affa3a1d78a313d2f7281a57afb1a34bae8ab412/seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7", size = 1457696, upload-time = "2024-01-25T13:21:52.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987", size = 294914, upload-time = "2024-01-25T13:21:49.598Z" }, +] + +[[package]] +name = "send2trash" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/f0/184b4b5f8d00f2a92cf96eec8967a3d550b52cf94362dad1100df9e48d57/send2trash-2.1.0.tar.gz", hash = "sha256:1c72b39f09457db3c05ce1d19158c2cbef4c32b8bedd02c155e49282b7ea7459", size = 17255, upload-time = "2026-01-14T06:27:36.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl", hash = "sha256:0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c", size = 17610, upload-time = "2026-01-14T06:27:35.218Z" }, +] + +[[package]] +name = "setuptools" +version = "82.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/f3/748f4d6f65d1756b9ae577f329c951cda23fb900e4de9f70900ced962085/setuptools-82.0.0.tar.gz", hash = "sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb", size = 1144893, upload-time = "2026-02-08T15:08:40.206Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0", size = 1003468, upload-time = "2026-02-08T15:08:38.723Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "smmap" +version = "5.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, +] + +[[package]] +name = "snowballstemmer" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, +] + +[[package]] +name = "sphinx" +version = "9.0.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version < '3.12' and sys_platform == 'emscripten'", + "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "alabaster", marker = "python_full_version < '3.12'" }, + { name = "babel", marker = "python_full_version < '3.12'" }, + { name = "colorama", marker = "python_full_version < '3.12' and sys_platform == 'win32'" }, + { name = "docutils", marker = "python_full_version < '3.12'" }, + { name = "imagesize", marker = "python_full_version < '3.12'" }, + { name = "jinja2", marker = "python_full_version < '3.12'" }, + { name = "packaging", marker = "python_full_version < '3.12'" }, + { name = "pygments", marker = "python_full_version < '3.12'" }, + { name = "requests", marker = "python_full_version < '3.12'" }, + { name = "roman-numerals", marker = "python_full_version < '3.12'" }, + { name = "snowballstemmer", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/3f/4bbd76424c393caead2e1eb89777f575dee5c8653e2d4b6afd7a564f5974/sphinx-9.0.4-py3-none-any.whl", hash = "sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb", size = 3917713, upload-time = "2025-12-04T07:45:24.944Z" }, +] + +[[package]] +name = "sphinx" +version = "9.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "alabaster", marker = "python_full_version >= '3.12'" }, + { name = "babel", marker = "python_full_version >= '3.12'" }, + { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "docutils", marker = "python_full_version >= '3.12'" }, + { name = "imagesize", marker = "python_full_version >= '3.12'" }, + { name = "jinja2", marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.12'" }, + { name = "pygments", marker = "python_full_version >= '3.12'" }, + { name = "requests", marker = "python_full_version >= '3.12'" }, + { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, + { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.46" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/aa/9ce0f3e7a9829ead5c8ce549392f33a12c4555a6c0609bb27d882e9c7ddf/sqlalchemy-2.0.46.tar.gz", hash = "sha256:cf36851ee7219c170bb0793dbc3da3e80c582e04a5437bc601bfe8c85c9216d7", size = 9865393, upload-time = "2026-01-21T18:03:45.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/ac/b42ad16800d0885105b59380ad69aad0cce5a65276e269ce2729a2343b6a/sqlalchemy-2.0.46-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:261c4b1f101b4a411154f1da2b76497d73abbfc42740029205d4d01fa1052684", size = 2154851, upload-time = "2026-01-21T18:27:30.54Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/d8710068cb79f64d002ebed62a7263c00c8fd95f4ebd4b5be8f7ca93f2bc/sqlalchemy-2.0.46-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:181903fe8c1b9082995325f1b2e84ac078b1189e2819380c2303a5f90e114a62", size = 3311241, upload-time = "2026-01-21T18:32:33.45Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/20c71487c7219ab3aa7421c7c62d93824c97c1460f2e8bb72404b0192d13/sqlalchemy-2.0.46-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:590be24e20e2424a4c3c1b0835e9405fa3d0af5823a1a9fc02e5dff56471515f", size = 3310741, upload-time = "2026-01-21T18:44:57.887Z" }, + { url = "https://files.pythonhosted.org/packages/65/80/d26d00b3b249ae000eee4db206fcfc564bf6ca5030e4747adf451f4b5108/sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7568fe771f974abadce52669ef3a03150ff03186d8eb82613bc8adc435a03f01", size = 3263116, upload-time = "2026-01-21T18:32:35.044Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/74dda7506640923821340541e8e45bd3edd8df78664f1f2e0aae8077192b/sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf7e1e78af38047e08836d33502c7a278915698b7c2145d045f780201679999", size = 3285327, upload-time = "2026-01-21T18:44:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/9f/25/6dcf8abafff1389a21c7185364de145107b7394ecdcb05233815b236330d/sqlalchemy-2.0.46-cp311-cp311-win32.whl", hash = "sha256:9d80ea2ac519c364a7286e8d765d6cd08648f5b21ca855a8017d9871f075542d", size = 2114564, upload-time = "2026-01-21T18:33:15.85Z" }, + { url = "https://files.pythonhosted.org/packages/93/5f/e081490f8523adc0088f777e4ebad3cac21e498ec8a3d4067074e21447a1/sqlalchemy-2.0.46-cp311-cp311-win_amd64.whl", hash = "sha256:585af6afe518732d9ccd3aea33af2edaae4a7aa881af5d8f6f4fe3a368699597", size = 2139233, upload-time = "2026-01-21T18:33:17.528Z" }, + { url = "https://files.pythonhosted.org/packages/b6/35/d16bfa235c8b7caba3730bba43e20b1e376d2224f407c178fbf59559f23e/sqlalchemy-2.0.46-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a9a72b0da8387f15d5810f1facca8f879de9b85af8c645138cba61ea147968c", size = 2153405, upload-time = "2026-01-21T19:05:54.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/6c/3192e24486749862f495ddc6584ed730c0c994a67550ec395d872a2ad650/sqlalchemy-2.0.46-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2347c3f0efc4de367ba00218e0ae5c4ba2306e47216ef80d6e31761ac97cb0b9", size = 3334702, upload-time = "2026-01-21T18:46:45.384Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a2/b9f33c8d68a3747d972a0bb758c6b63691f8fb8a49014bc3379ba15d4274/sqlalchemy-2.0.46-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9094c8b3197db12aa6f05c51c05daaad0a92b8c9af5388569847b03b1007fb1b", size = 3347664, upload-time = "2026-01-21T18:40:09.979Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d2/3e59e2a91eaec9db7e8dc6b37b91489b5caeb054f670f32c95bcba98940f/sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37fee2164cf21417478b6a906adc1a91d69ae9aba8f9533e67ce882f4bb1de53", size = 3277372, upload-time = "2026-01-21T18:46:47.168Z" }, + { url = "https://files.pythonhosted.org/packages/dd/dd/67bc2e368b524e2192c3927b423798deda72c003e73a1e94c21e74b20a85/sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b1e14b2f6965a685c7128bd315e27387205429c2e339eeec55cb75ca4ab0ea2e", size = 3312425, upload-time = "2026-01-21T18:40:11.548Z" }, + { url = "https://files.pythonhosted.org/packages/43/82/0ecd68e172bfe62247e96cb47867c2d68752566811a4e8c9d8f6e7c38a65/sqlalchemy-2.0.46-cp312-cp312-win32.whl", hash = "sha256:412f26bb4ba942d52016edc8d12fb15d91d3cd46b0047ba46e424213ad407bcb", size = 2113155, upload-time = "2026-01-21T18:42:49.748Z" }, + { url = "https://files.pythonhosted.org/packages/bc/2a/2821a45742073fc0331dc132552b30de68ba9563230853437cac54b2b53e/sqlalchemy-2.0.46-cp312-cp312-win_amd64.whl", hash = "sha256:ea3cd46b6713a10216323cda3333514944e510aa691c945334713fca6b5279ff", size = 2140078, upload-time = "2026-01-21T18:42:51.197Z" }, + { url = "https://files.pythonhosted.org/packages/b3/4b/fa7838fe20bb752810feed60e45625a9a8b0102c0c09971e2d1d95362992/sqlalchemy-2.0.46-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93a12da97cca70cea10d4b4fc602589c4511f96c1f8f6c11817620c021d21d00", size = 2150268, upload-time = "2026-01-21T19:05:56.621Z" }, + { url = "https://files.pythonhosted.org/packages/46/c1/b34dccd712e8ea846edf396e00973dda82d598cb93762e55e43e6835eba9/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af865c18752d416798dae13f83f38927c52f085c52e2f32b8ab0fef46fdd02c2", size = 3276511, upload-time = "2026-01-21T18:46:49.022Z" }, + { url = "https://files.pythonhosted.org/packages/96/48/a04d9c94753e5d5d096c628c82a98c4793b9c08ca0e7155c3eb7d7db9f24/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d679b5f318423eacb61f933a9a0f75535bfca7056daeadbf6bd5bcee6183aee", size = 3292881, upload-time = "2026-01-21T18:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/be/f4/06eda6e91476f90a7d8058f74311cb65a2fb68d988171aced81707189131/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64901e08c33462acc9ec3bad27fc7a5c2b6491665f2aa57564e57a4f5d7c52ad", size = 3224559, upload-time = "2026-01-21T18:46:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a2/d2af04095412ca6345ac22b33b89fe8d6f32a481e613ffcb2377d931d8d0/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8ac45e8f4eaac0f9f8043ea0e224158855c6a4329fd4ee37c45c61e3beb518e", size = 3262728, upload-time = "2026-01-21T18:40:14.883Z" }, + { url = "https://files.pythonhosted.org/packages/31/48/1980c7caa5978a3b8225b4d230e69a2a6538a3562b8b31cea679b6933c83/sqlalchemy-2.0.46-cp313-cp313-win32.whl", hash = "sha256:8d3b44b3d0ab2f1319d71d9863d76eeb46766f8cf9e921ac293511804d39813f", size = 2111295, upload-time = "2026-01-21T18:42:52.366Z" }, + { url = "https://files.pythonhosted.org/packages/2d/54/f8d65bbde3d877617c4720f3c9f60e99bb7266df0d5d78b6e25e7c149f35/sqlalchemy-2.0.46-cp313-cp313-win_amd64.whl", hash = "sha256:77f8071d8fbcbb2dd11b7fd40dedd04e8ebe2eb80497916efedba844298065ef", size = 2137076, upload-time = "2026-01-21T18:42:53.924Z" }, + { url = "https://files.pythonhosted.org/packages/56/ba/9be4f97c7eb2b9d5544f2624adfc2853e796ed51d2bb8aec90bc94b7137e/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1e8cc6cc01da346dc92d9509a63033b9b1bda4fed7a7a7807ed385c7dccdc10", size = 3556533, upload-time = "2026-01-21T18:33:06.636Z" }, + { url = "https://files.pythonhosted.org/packages/20/a6/b1fc6634564dbb4415b7ed6419cdfeaadefd2c39cdab1e3aa07a5f2474c2/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96c7cca1a4babaaf3bfff3e4e606e38578856917e52f0384635a95b226c87764", size = 3523208, upload-time = "2026-01-21T18:45:08.436Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d8/41e0bdfc0f930ff236f86fccd12962d8fa03713f17ed57332d38af6a3782/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2a9f9aee38039cf4755891a1e50e1effcc42ea6ba053743f452c372c3152b1b", size = 3464292, upload-time = "2026-01-21T18:33:08.208Z" }, + { url = "https://files.pythonhosted.org/packages/f0/8b/9dcbec62d95bea85f5ecad9b8d65b78cc30fb0ffceeb3597961f3712549b/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db23b1bf8cfe1f7fda19018e7207b20cdb5168f83c437ff7e95d19e39289c447", size = 3473497, upload-time = "2026-01-21T18:45:10.552Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f8/5ecdfc73383ec496de038ed1614de9e740a82db9ad67e6e4514ebc0708a3/sqlalchemy-2.0.46-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:56bdd261bfd0895452006d5316cbf35739c53b9bb71a170a331fa0ea560b2ada", size = 2152079, upload-time = "2026-01-21T19:05:58.477Z" }, + { url = "https://files.pythonhosted.org/packages/e5/bf/eba3036be7663ce4d9c050bc3d63794dc29fbe01691f2bf5ccb64e048d20/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33e462154edb9493f6c3ad2125931e273bbd0be8ae53f3ecd1c161ea9a1dd366", size = 3272216, upload-time = "2026-01-21T18:46:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/05/45/1256fb597bb83b58a01ddb600c59fe6fdf0e5afe333f0456ed75c0f8d7bd/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcdce05f056622a632f1d44bb47dbdb677f58cad393612280406ce37530eb6d", size = 3277208, upload-time = "2026-01-21T18:40:16.38Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a0/2053b39e4e63b5d7ceb3372cface0859a067c1ddbd575ea7e9985716f771/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e84b09a9b0f19accedcbeff5c2caf36e0dd537341a33aad8d680336152dc34e", size = 3221994, upload-time = "2026-01-21T18:46:54.622Z" }, + { url = "https://files.pythonhosted.org/packages/1e/87/97713497d9502553c68f105a1cb62786ba1ee91dea3852ae4067ed956a50/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4f52f7291a92381e9b4de9050b0a65ce5d6a763333406861e33906b8aa4906bf", size = 3243990, upload-time = "2026-01-21T18:40:18.253Z" }, + { url = "https://files.pythonhosted.org/packages/a8/87/5d1b23548f420ff823c236f8bea36b1a997250fd2f892e44a3838ca424f4/sqlalchemy-2.0.46-cp314-cp314-win32.whl", hash = "sha256:70ed2830b169a9960193f4d4322d22be5c0925357d82cbf485b3369893350908", size = 2114215, upload-time = "2026-01-21T18:42:55.232Z" }, + { url = "https://files.pythonhosted.org/packages/3a/20/555f39cbcf0c10cf452988b6a93c2a12495035f68b3dbd1a408531049d31/sqlalchemy-2.0.46-cp314-cp314-win_amd64.whl", hash = "sha256:3c32e993bc57be6d177f7d5d31edb93f30726d798ad86ff9066d75d9bf2e0b6b", size = 2139867, upload-time = "2026-01-21T18:42:56.474Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f0/f96c8057c982d9d8a7a68f45d69c674bc6f78cad401099692fe16521640a/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4dafb537740eef640c4d6a7c254611dca2df87eaf6d14d6a5fca9d1f4c3fc0fa", size = 3561202, upload-time = "2026-01-21T18:33:10.337Z" }, + { url = "https://files.pythonhosted.org/packages/d7/53/3b37dda0a5b137f21ef608d8dfc77b08477bab0fe2ac9d3e0a66eaeab6fc/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42a1643dc5427b69aca967dae540a90b0fbf57eaf248f13a90ea5930e0966863", size = 3526296, upload-time = "2026-01-21T18:45:12.657Z" }, + { url = "https://files.pythonhosted.org/packages/33/75/f28622ba6dde79cd545055ea7bd4062dc934e0621f7b3be2891f8563f8de/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ff33c6e6ad006bbc0f34f5faf941cfc62c45841c64c0a058ac38c799f15b5ede", size = 3470008, upload-time = "2026-01-21T18:33:11.725Z" }, + { url = "https://files.pythonhosted.org/packages/a9/42/4afecbbc38d5e99b18acef446453c76eec6fbd03db0a457a12a056836e22/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:82ec52100ec1e6ec671563bbd02d7c7c8d0b9e71a0723c72f22ecf52d1755330", size = 3476137, upload-time = "2026-01-21T18:45:15.001Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a1/9c4efa03300926601c19c18582531b45aededfb961ab3c3585f1e24f120b/sqlalchemy-2.0.46-py3-none-any.whl", hash = "sha256:f9c11766e7e7c0a2767dda5acb006a118640c9fc0a4104214b96269bfb78399e", size = 1937882, upload-time = "2026-01-21T18:22:10.456Z" }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, +] + +[[package]] +name = "tabulate" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, +] + +[[package]] +name = "terminado" +version = "0.18.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess", marker = "os_name != 'nt'" }, + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "tornado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/11/965c6fd8e5cc254f1fe142d547387da17a8ebfd75a3455f637c663fb38a0/terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e", size = 32701, upload-time = "2024-03-12T14:34:39.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0", size = 14154, upload-time = "2024-03-12T14:34:36.569Z" }, +] + +[[package]] +name = "tinycss2" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085, upload-time = "2024-10-24T14:58:29.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "toolz" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/d6/114b492226588d6ff54579d95847662fc69196bdeec318eb45393b24c192/toolz-1.1.0.tar.gz", hash = "sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b", size = 52613, upload-time = "2025-10-17T04:03:21.661Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl", hash = "sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8", size = 58093, upload-time = "2025-10-17T04:03:20.435Z" }, +] + +[[package]] +name = "tornado" +version = "6.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/1d/0a336abf618272d53f62ebe274f712e213f5a03c0b2339575430b8362ef2/tornado-6.5.4.tar.gz", hash = "sha256:a22fa9047405d03260b483980635f0b041989d8bcc9a313f8fe18b411d84b1d7", size = 513632, upload-time = "2025-12-15T19:21:03.836Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/a9/e94a9d5224107d7ce3cc1fab8d5dc97f5ea351ccc6322ee4fb661da94e35/tornado-6.5.4-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d6241c1a16b1c9e4cc28148b1cda97dd1c6cb4fb7068ac1bedc610768dff0ba9", size = 443909, upload-time = "2025-12-15T19:20:48.382Z" }, + { url = "https://files.pythonhosted.org/packages/db/7e/f7b8d8c4453f305a51f80dbb49014257bb7d28ccb4bbb8dd328ea995ecad/tornado-6.5.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2d50f63dda1d2cac3ae1fa23d254e16b5e38153758470e9956cbc3d813d40843", size = 442163, upload-time = "2025-12-15T19:20:49.791Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b5/206f82d51e1bfa940ba366a8d2f83904b15942c45a78dd978b599870ab44/tornado-6.5.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cf66105dc6acb5af613c054955b8137e34a03698aa53272dbda4afe252be17", size = 445746, upload-time = "2025-12-15T19:20:51.491Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9d/1a3338e0bd30ada6ad4356c13a0a6c35fbc859063fa7eddb309183364ac1/tornado-6.5.4-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50ff0a58b0dc97939d29da29cd624da010e7f804746621c78d14b80238669335", size = 445083, upload-time = "2025-12-15T19:20:52.778Z" }, + { url = "https://files.pythonhosted.org/packages/50/d4/e51d52047e7eb9a582da59f32125d17c0482d065afd5d3bc435ff2120dc5/tornado-6.5.4-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5fb5e04efa54cf0baabdd10061eb4148e0be137166146fff835745f59ab9f7f", size = 445315, upload-time = "2025-12-15T19:20:53.996Z" }, + { url = "https://files.pythonhosted.org/packages/27/07/2273972f69ca63dbc139694a3fc4684edec3ea3f9efabf77ed32483b875c/tornado-6.5.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9c86b1643b33a4cd415f8d0fe53045f913bf07b4a3ef646b735a6a86047dda84", size = 446003, upload-time = "2025-12-15T19:20:56.101Z" }, + { url = "https://files.pythonhosted.org/packages/d1/83/41c52e47502bf7260044413b6770d1a48dda2f0246f95ee1384a3cd9c44a/tornado-6.5.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:6eb82872335a53dd063a4f10917b3efd28270b56a33db69009606a0312660a6f", size = 445412, upload-time = "2025-12-15T19:20:57.398Z" }, + { url = "https://files.pythonhosted.org/packages/10/c7/bc96917f06cbee182d44735d4ecde9c432e25b84f4c2086143013e7b9e52/tornado-6.5.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6076d5dda368c9328ff41ab5d9dd3608e695e8225d1cd0fd1e006f05da3635a8", size = 445392, upload-time = "2025-12-15T19:20:58.692Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1a/d7592328d037d36f2d2462f4bc1fbb383eec9278bc786c1b111cbbd44cfa/tornado-6.5.4-cp39-abi3-win32.whl", hash = "sha256:1768110f2411d5cd281bac0a090f707223ce77fd110424361092859e089b38d1", size = 446481, upload-time = "2025-12-15T19:21:00.008Z" }, + { url = "https://files.pythonhosted.org/packages/d6/6d/c69be695a0a64fd37a97db12355a035a6d90f79067a3cf936ec2b1dc38cd/tornado-6.5.4-cp39-abi3-win_amd64.whl", hash = "sha256:fa07d31e0cd85c60713f2b995da613588aa03e1303d75705dca6af8babc18ddc", size = 446886, upload-time = "2025-12-15T19:21:01.287Z" }, + { url = "https://files.pythonhosted.org/packages/50/49/8dc3fd90902f70084bd2cd059d576ddb4f8bb44c2c7c0e33a11422acb17e/tornado-6.5.4-cp39-abi3-win_arm64.whl", hash = "sha256:053e6e16701eb6cbe641f308f4c1a9541f91b6261991160391bfc342e8a551a1", size = 445910, upload-time = "2025-12-15T19:21:02.571Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, +] + +[[package]] +name = "types-markdown" +version = "3.10.2.20260211" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/2e/35b30a09f6ee8a69142408d3ceb248c4454aa638c0a414d8704a3ef79563/types_markdown-3.10.2.20260211.tar.gz", hash = "sha256:66164310f88c11a58c6c706094c6f8c537c418e3525d33b76276a5fbd66b01ce", size = 19768, upload-time = "2026-02-11T04:19:29.497Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/c9/659fa2df04b232b0bfcd05d2418e683080e91ec68f636f3c0a5a267350e7/types_markdown-3.10.2.20260211-py3-none-any.whl", hash = "sha256:2d94d08587e3738203b3c4479c449845112b171abe8b5cadc9b0c12fcf3e99da", size = 25854, upload-time = "2026-02-11T04:19:28.647Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + +[[package]] +name = "uc-micro-py" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/7a/146a99696aee0609e3712f2b44c6274566bc368dfe8375191278045186b8/uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a", size = 6043, upload-time = "2024-02-09T16:52:01.654Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/87/1f677586e8ac487e29672e4b17455758fce261de06a0d086167bb760361a/uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5", size = 6229, upload-time = "2024-02-09T16:52:00.371Z" }, +] + +[[package]] +name = "uncertainties" +version = "3.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/0c/cb09f33b26955399c675ab378e4063ed7e48422d3d49f96219ab0be5eba9/uncertainties-3.2.3.tar.gz", hash = "sha256:76a5653e686f617a42922d546a239e9efce72e6b35411b7750a1d12dcba03031", size = 160492, upload-time = "2025-04-21T19:58:28.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl", hash = "sha256:313353900d8f88b283c9bad81e7d2b2d3d4bcc330cbace35403faaed7e78890a", size = 60118, upload-time = "2025-04-21T19:58:26.864Z" }, +] + +[[package]] +name = "uri-template" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/c7/0336f2bd0bcbada6ccef7aaa25e443c118a704f828a0620c6fa0207c1b64/uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7", size = 21678, upload-time = "2023-06-21T01:49:05.374Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363", size = 11140, upload-time = "2023-06-21T01:49:03.467Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "versioningit" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/f4/bc578cc80989c572231a36cc03cc097091176fa3fb8b4e2af1deb4370eb7/versioningit-3.3.0.tar.gz", hash = "sha256:b91ad7d73e73d21220e69540f20213f2b729a1f9b35c04e9e137eaf28d2214da", size = 220280, upload-time = "2025-06-27T20:13:23.368Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl", hash = "sha256:23b1db3c4756cded9bd6b0ddec6643c261e3d0c471707da3e0b230b81ce53e4b", size = 38439, upload-time = "2025-06-27T20:13:21.927Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, +] + +[[package]] +name = "webcolors" +version = "25.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/7a/eb316761ec35664ea5174709a68bbd3389de60d4a1ebab8808bfc264ed67/webcolors-25.10.0.tar.gz", hash = "sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf", size = 53491, upload-time = "2025-10-31T07:51:03.977Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl", hash = "sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d", size = 14905, upload-time = "2025-10-31T07:51:01.778Z" }, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, +] + +[[package]] +name = "websocket-client" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + +[[package]] +name = "widgetsnbextension" +version = "4.0.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/f4/c67440c7fb409a71b7404b7aefcd7569a9c0d6bd071299bf4198ae7a5d95/widgetsnbextension-4.0.15.tar.gz", hash = "sha256:de8610639996f1567952d763a5a41af8af37f2575a41f9852a38f947eb82a3b9", size = 1097402, upload-time = "2025-11-01T21:15:55.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl", hash = "sha256:8156704e4346a571d9ce73b84bee86a29906c9abfd7223b7228a28899ccf3366", size = 2196503, upload-time = "2025-11-01T21:15:53.565Z" }, +] + +[[package]] +name = "xarray" +version = "2026.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/85/113ff1e2cde9e8a5b13c2f0ef4e9f5cd6ca3a036b6452f4dd523419289b5/xarray-2026.1.0.tar.gz", hash = "sha256:0c9814761f9d9a9545df37292d3fda89f83201f3e02ae0f09f03313d9cfdd5e2", size = 3107024, upload-time = "2026-01-28T17:49:03.822Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/8e/952a351c10df395d9bab850f611f4368834ae9104d6449049f5a49e00925/xarray-2026.1.0-py3-none-any.whl", hash = "sha256:5fcc03d3ed8dfb662aa254efe6cd65efc70014182bbc2126e4b90d291d970d41", size = 1403009, upload-time = "2026-01-28T17:49:01.538Z" }, +] + +[[package]] +name = "xyzservices" +version = "2025.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/0f/022795fc1201e7c29e742a509913badb53ce0b38f64b6db859e2f6339da9/xyzservices-2025.11.0.tar.gz", hash = "sha256:2fc72b49502b25023fd71e8f532fb4beddbbf0aa124d90ea25dba44f545e17ce", size = 1135703, upload-time = "2025-11-22T11:31:51.82Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/5c/2c189d18d495dd0fa3f27ccc60762bbc787eed95b9b0147266e72bb76585/xyzservices-2025.11.0-py3-none-any.whl", hash = "sha256:de66a7599a8d6dad63980b77defd1d8f5a5a9cb5fc8774ea1c6e89ca7c2a3d2f", size = 93916, upload-time = "2025-11-22T11:31:50.525Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +]