diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..92c2562 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,70 @@ +name: CI + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.9", "3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + version: "latest" + + - name: Set up Python ${{ matrix.python-version }} + run: uv python install ${{ matrix.python-version }} + + - name: Install dependencies + run: uv sync --dev + + - name: Lint with ruff + run: uv run ruff check . + + - name: Check formatting with ruff + run: uv run ruff format --check . + + - name: Test with pytest + run: uv run pytest --cov=phylib --cov-report=xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + file: ./coverage.xml + fail_ci_if_error: true + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + build: + runs-on: ubuntu-latest + needs: test + + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + version: "latest" + + - name: Set up Python + run: uv python install 3.9 + + - name: Build package + run: uv build + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ \ No newline at end of file diff --git a/Makefile b/Makefile index 33c32ca..820889a 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ clean-build: rm -fr build/ rm -fr dist/ rm -fr *.egg-info + rm -fr .eggs/ clean-pyc: find . -name '*.pyc' -exec rm -f {} + @@ -9,22 +10,53 @@ clean-pyc: find . -name '*~' -exec rm -f {} + find . -name '__pycache__' -exec rm -fr {} + -clean: clean-build clean-pyc +clean-test: + rm -fr .tox/ + rm -f .coverage + rm -fr htmlcov/ + rm -fr .pytest_cache/ + rm -fr .ruff_cache/ + +clean: clean-build clean-pyc clean-test + +install: + uv sync --dev lint: - flake8 phylib + uv run ruff check phylib + +format: + uv run ruff format phylib + +format-check: + uv run ruff format --check phylib + +lint-fix: + uv run ruff check --fix phylib -test: lint - py.test --cov-report term-missing --cov=phylib phylib +test: lint format-check + uv run pytest --cov-report term-missing --cov=phylib phylib + +test-fast: + uv run pytest phylib coverage: - coverage --html + uv run coverage html apidoc: - python tools/api.py + uv run python tools/api.py build: - python setup.py sdist --formats=zip + uv build upload: - python setup.py sdist --formats=zip upload + uv publish + +upload-test: + uv publish --publish-url https://test.pypi.org/legacy/ + +dev: install lint format test + +ci: lint format-check test build + +.PHONY: clean-build clean-pyc clean-test clean install lint format format-check lint-fix test test-fast coverage apidoc build upload upload-test dev ci \ No newline at end of file diff --git a/.travis.yml b/deprecated/.travis.yml similarity index 100% rename from .travis.yml rename to deprecated/.travis.yml diff --git a/environment.yml b/deprecated/environment.yml similarity index 100% rename from environment.yml rename to deprecated/environment.yml diff --git a/requirements-dev.txt b/deprecated/requirements-dev.txt similarity index 100% rename from requirements-dev.txt rename to deprecated/requirements-dev.txt diff --git a/requirements.txt b/deprecated/requirements.txt similarity index 100% rename from requirements.txt rename to deprecated/requirements.txt diff --git a/setup.cfg b/deprecated/setup.cfg similarity index 100% rename from setup.cfg rename to deprecated/setup.cfg diff --git a/setup.py b/deprecated/setup.py similarity index 66% rename from setup.py rename to deprecated/setup.py index 4653e86..bd7ea92 100644 --- a/setup.py +++ b/deprecated/setup.py @@ -4,9 +4,9 @@ """Installation script.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import os import os.path as op @@ -16,15 +16,18 @@ from setuptools import setup -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Setup -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _package_tree(pkgroot): path = op.dirname(__file__) - subdirs = [op.relpath(i[0], path).replace(op.sep, '.') - for i in os.walk(op.join(path, pkgroot)) - if '__init__.py' in i[2]] + subdirs = [ + op.relpath(i[0], path).replace(op.sep, '.') + for i in os.walk(op.join(path, pkgroot)) + if '__init__.py' in i[2] + ] return subdirs @@ -41,7 +44,7 @@ def _package_tree(pkgroot): setup( name='phylib', version=version, - license="BSD", + license='BSD', description='Ephys data analysis for thousands of channels', long_description=readme, long_description_content_type='text/markdown', @@ -52,8 +55,17 @@ def _package_tree(pkgroot): package_dir={'phylib': 'phylib'}, package_data={ 'phylib': [ - '*.vert', '*.frag', '*.glsl', '*.npy', '*.gz', '*.txt', - '*.html', '*.css', '*.js', '*.prb'], + '*.vert', + '*.frag', + '*.glsl', + '*.npy', + '*.gz', + '*.txt', + '*.html', + '*.css', + '*.js', + '*.prb', + ], }, include_package_data=True, keywords='phy,data analysis,electrophysiology,neuroscience', @@ -62,7 +74,7 @@ def _package_tree(pkgroot): 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Natural Language :: English', - "Framework :: IPython", + 'Framework :: IPython', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.7', ], diff --git a/phylib/__init__.py b/phylib/__init__.py index d9b76f6..414a76f 100644 --- a/phylib/__init__.py +++ b/phylib/__init__.py @@ -4,9 +4,9 @@ """Utilities for large-scale ephys data analysis.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import atexit import logging @@ -16,9 +16,9 @@ from .utils.event import connect, unconnect, emit -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Global variables and functions -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ __author__ = 'Cyrille Rossant' __email__ = 'cyrille.rossant at gmail.com' @@ -43,10 +43,10 @@ def format(self, record): # Only keep the first character in the level name. record.levelname = record.levelname[0] filename = op.splitext(op.basename(record.pathname))[0] - record.caller = '{:s}:{:d}'.format(filename, record.lineno).ljust(20) + record.caller = f'{filename:s}:{record.lineno:d}'.ljust(20) message = super(_Formatter, self).format(record) color_code = self.color_codes.get(record.levelname, '90') - message = '\33[%sm%s\33[0m' % (color_code, message) + message = f'\x1b[{color_code}m{message}\x1b[0m' return message @@ -80,4 +80,5 @@ def on_exit(): # pragma: no cover def test(): # pragma: no cover """Run the full testing suite of phylib.""" import pytest + pytest.main() diff --git a/phylib/conftest.py b/phylib/conftest.py index 4a87950..9089dd3 100644 --- a/phylib/conftest.py +++ b/phylib/conftest.py @@ -1,27 +1,24 @@ -# -*- coding: utf-8 -*- - """py.test utilities.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import logging import os -from pathlib import Path -import tempfile import shutil +import tempfile import warnings +from pathlib import Path import numpy as np from pytest import fixture from phylib import add_default_handler - -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Common fixtures -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ logger = logging.getLogger('phylib') logger.setLevel(10) @@ -30,8 +27,8 @@ # Fix the random seed in the tests. np.random.seed(2015) -warnings.filterwarnings("ignore", message="numpy.dtype size changed") -warnings.filterwarnings("ignore", message="numpy.ufunc size changed") +warnings.filterwarnings('ignore', message='numpy.dtype size changed') +warnings.filterwarnings('ignore', message='numpy.ufunc size changed') @fixture diff --git a/phylib/electrode/mea.py b/phylib/electrode/mea.py index 2ba377b..a96937e 100644 --- a/phylib/electrode/mea.py +++ b/phylib/electrode/mea.py @@ -1,24 +1,22 @@ -# -*- coding: utf-8 -*- - """Multi-electrode arrays.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import itertools from pathlib import Path import numpy as np -from phylib.utils._types import _as_array from phylib.utils._misc import read_python +from phylib.utils._types import _as_array - -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # PRB file utilities -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _edges_to_adjacency_list(edges): """Convert a list of edges into an adjacency list.""" @@ -38,8 +36,7 @@ def _edges_to_adjacency_list(edges): def _adjacency_subset(adjacency, subset): - return {c: [v for v in vals if v in subset] - for (c, vals) in adjacency.items() if c in subset} + return {c: [v for v in vals if v in subset] for (c, vals) in adjacency.items() if c in subset} def _remap_adjacency(adjacency, mapping): @@ -76,8 +73,7 @@ def _probe_adjacency_list(probe): def _channels_per_group(probe): groups = probe['channel_groups'].keys() - return {group: probe['channel_groups'][group]['channels'] - for group in groups} + return {group: probe['channel_groups'][group]['channels'] for group in groups} def load_probe(name_or_path): @@ -85,9 +81,9 @@ def load_probe(name_or_path): path = Path(name_or_path) # The argument can be either a path to a PRB file or the name of a built-in probe.. if not path.exists(): - path = Path(__file__).parent / ('probes/%s.prb' % name_or_path) + path = Path(__file__).parent / (f'probes/{name_or_path}.prb') if not path.exists(): - raise IOError("The probe `{}` cannot be found.".format(name_or_path)) + raise OSError(f'The probe `{name_or_path}` cannot be found.') return MEA(probe=read_python(path)) @@ -97,11 +93,12 @@ def list_probes(): return [fn.stem for fn in path.glob('*.prb')] -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # MEA class -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + -class MEA(object): +class MEA: """A Multi-Electrode Array. There are two modes: @@ -112,12 +109,13 @@ class MEA(object): """ - def __init__(self, - channels=None, - positions=None, - adjacency=None, - probe=None, - ): + def __init__( + self, + channels=None, + positions=None, + adjacency=None, + probe=None, + ): self._probe = probe self._channels = channels self._check_positions(positions) @@ -137,11 +135,11 @@ def _check_positions(self, positions): return positions = _as_array(positions) if positions.shape[0] != self.n_channels: - raise ValueError("'positions' " - "(shape {0:s})".format(str(positions.shape)) + - " and 'n_channels' " - "({0:d})".format(self.n_channels) + - " do not match.") + raise ValueError( + f"'positions' (shape {str(positions.shape):s})" + + f" and 'n_channels' ({self.n_channels:d})" + + ' do not match.' + ) @property def positions(self): diff --git a/phylib/electrode/tests/test_mea.py b/phylib/electrode/tests/test_mea.py index 895d85c..441c51b 100644 --- a/phylib/electrode/tests/test_mea.py +++ b/phylib/electrode/tests/test_mea.py @@ -1,26 +1,30 @@ -# -*- coding: utf-8 -*- - """Test MEA.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ from pathlib import Path -from pytest import raises import numpy as np from numpy.testing import assert_array_equal as ae +from pytest import raises -from ..mea import (_probe_channels, _remap_adjacency, _adjacency_subset, - _probe_positions, _probe_adjacency_list, - MEA, load_probe, list_probes - ) - - -#------------------------------------------------------------------------------ +from ..mea import ( + MEA, + _adjacency_subset, + _probe_adjacency_list, + _probe_channels, + _probe_positions, + _remap_adjacency, + list_probes, + load_probe, +) + +# ------------------------------------------------------------------------------ # Tests -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_remap(): adjacency = {1: [2, 3, 7], 3: [5, 11]} @@ -41,19 +45,24 @@ def test_adjacency_subset(): def test_probe(): - probe = {'channel_groups': { - 0: {'channels': [0, 3, 1], - 'graph': [[0, 3], [1, 0]], - 'geometry': {0: (10, 10), 1: (10, 20), 3: (20, 30)}, - }, - 1: {'channels': [7], - 'graph': [], - }, - }} - adjacency = {0: set([1, 3]), - 1: set([0]), - 3: set([0]), - } + probe = { + 'channel_groups': { + 0: { + 'channels': [0, 3, 1], + 'graph': [[0, 3], [1, 0]], + 'geometry': {0: (10, 10), 1: (10, 20), 3: (20, 30)}, + }, + 1: { + 'channels': [7], + 'graph': [], + }, + } + } + adjacency = { + 0: {1, 3}, + 1: {0}, + 3: {0}, + } assert _probe_channels(probe, 0) == [0, 3, 1] ae(_probe_positions(probe, 0), [(10, 10), (20, 30), (10, 20)]) assert _probe_adjacency_list(probe) == adjacency @@ -68,7 +77,6 @@ def test_probe(): def test_mea(): - n_channels = 10 channels = np.arange(n_channels) positions = np.random.randn(n_channels, 2) diff --git a/phylib/io/__init__.py b/phylib/io/__init__.py index 0387c7a..b787f9d 100644 --- a/phylib/io/__init__.py +++ b/phylib/io/__init__.py @@ -5,5 +5,10 @@ from .array import SpikeSelector from .traces import ( - get_ephys_reader, get_spike_waveforms, NpyWriter, - extract_waveforms, iter_waveforms, export_waveforms) + get_ephys_reader, + get_spike_waveforms, + NpyWriter, + extract_waveforms, + iter_waveforms, + export_waveforms, +) diff --git a/phylib/io/alf.py b/phylib/io/alf.py index f6a7b09..a91756b 100644 --- a/phylib/io/alf.py +++ b/phylib/io/alf.py @@ -1,31 +1,29 @@ -# -*- coding: utf-8 -*- - """ALF dataset generation.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +import ast import logging -from pathlib import Path import shutil -import ast import uuid +from pathlib import Path -from tqdm import tqdm import numpy as np +from tqdm import tqdm -from phylib.utils._misc import _read_tsv_simple, ensure_dir_exists from phylib.io.array import _spikes_per_cluster, _unique from phylib.io.model import load_model +from phylib.utils._misc import _read_tsv_simple, ensure_dir_exists logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # File utils -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ NSAMPLE_WAVEFORMS = 500 # number of waveforrms sampled out of the raw data @@ -68,10 +66,10 @@ def _create_if_possible(path, new_path, force=False): """Prepare the copy/move/symlink of a file, by making sure the source exists while the destination does not.""" if not Path(path).exists(): # pragma: no cover - logger.warning("Path %s does not exist, skipping.", path) + logger.warning('Path %s does not exist, skipping.', path) return False if Path(new_path).exists() and not force: # pragma: no cover - logger.warning("Path %s already exists, skipping.", new_path) + logger.warning('Path %s already exists, skipping.', new_path) return False ensure_dir_exists(new_path.parent) return True @@ -80,7 +78,7 @@ def _create_if_possible(path, new_path, force=False): def _copy_if_possible(path, new_path, force=False): if not _create_if_possible(path, new_path, force=force): return False - logger.debug("Copying %s to %s.", path, new_path) + logger.debug('Copying %s to %s.', path, new_path) shutil.copy(path, new_path) return True @@ -96,11 +94,12 @@ def _load(path): return np.fromfile(path, np.int16) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Ephys ALF creator -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + -class EphysAlfCreator(object): +class EphysAlfCreator: """Class for converting a dataset in KS/phy format into ALF.""" def __init__(self, model): @@ -111,16 +110,16 @@ def __init__(self, model): def convert(self, out_path, force=False, label='', ampfactor=1): """Convert from KS/phy format to ALF.""" - logger.info("Converting dataset to ALF.") + logger.info('Converting dataset to ALF.') self.out_path = Path(out_path) self.label = label self.ampfactor = ampfactor if self.out_path.resolve() == self.dir_path.resolve(): - raise IOError("The source and target directories cannot be the same.") + raise OSError('The source and target directories cannot be the same.') if not self.out_path.exists(): self.out_path.mkdir() - with tqdm(desc="Converting to ALF", total=135) as bar: + with tqdm(desc='Converting to ALF', total=135) as bar: bar.update(10) self.make_cluster_objects() bar.update(10) @@ -128,8 +127,7 @@ def convert(self, out_path, force=False, label='', ampfactor=1): bar.update(5) self.make_template_and_spikes_objects() bar.update(30) - self.model.save_spikes_subset_waveforms( - NSAMPLE_WAVEFORMS, sample2unit=self.ampfactor) + self.model.save_spikes_subset_waveforms(NSAMPLE_WAVEFORMS, sample2unit=self.ampfactor) bar.update(50) self.make_depths() bar.update(20) @@ -190,7 +188,12 @@ def make_cluster_objects(self): # group by average over cluster number # camps = np.zeros(self.model.templates_channels.shape[0],) * np.nan - camps = np.zeros(self.model.clusters_channels.shape[0], ) * np.nan + camps = ( + np.zeros( + self.model.clusters_channels.shape[0], + ) + * np.nan + ) camps[self.cluster_ids] = self.model.clusters_amplitudes amps_path = self.dir_path / 'clusters.amps.npy' self._save_npy(amps_path.name, camps * self.ampfactor) @@ -243,13 +246,14 @@ def make_template_and_spikes_objects(self): # and not seconds self._save_npy('spikes.times.npy', self.model.spike_times) self._save_npy('spikes.samples.npy', self.model.spike_samples) - spike_amps, templates_v, template_amps = self.model.get_amplitudes_true(self.ampfactor, - use='templates') + spike_amps, templates_v, template_amps = self.model.get_amplitudes_true( + self.ampfactor, use='templates' + ) self._save_npy('spikes.amps.npy', spike_amps, np.float32) self._save_npy('templates.amps.npy', template_amps) if self.model.sparse_templates.cols: - raise NotImplementedError("Sparse template export to ALF not implemented yet") + raise NotImplementedError('Sparse template export to ALF not implemented yet') else: n_templates, n_wavsamps, nchall = templates_v.shape # for some datasets, 32 may be too much @@ -260,17 +264,22 @@ def make_template_and_spikes_objects(self): # for each template, find the nearest channels to keep (one the same probe...) for t in np.arange(n_templates): current_probe = self.model.channel_probes[self.model.templates_channels[t]] - channel_distance = np.sum(np.abs( - self.model.channel_positions - - self.model.channel_positions[self.model.templates_channels[t]]), axis=1) + channel_distance = np.sum( + np.abs( + self.model.channel_positions + - self.model.channel_positions[self.model.templates_channels[t]] + ), + axis=1, + ) channel_distance[self.model.channel_probes != current_probe] += np.inf templates_inds[t, :] = np.argsort(channel_distance)[:ncw] templates[t, ...] = templates_v[t, :][:, templates_inds[t, :]] np.save(self.out_path.joinpath('templates.waveforms'), templates) np.save(self.out_path.joinpath('templates.waveformsChannels'), templates_inds) - _, clusters_v, cluster_amps = self.model.get_amplitudes_true(self.ampfactor, - use='clusters') + _, clusters_v, cluster_amps = self.model.get_amplitudes_true( + self.ampfactor, use='clusters' + ) n_clusters, n_wavsamps, nchall = clusters_v.shape # for some datasets, 32 may be too much ncw = min(self.model.n_closest_channels, nchall) @@ -282,9 +291,12 @@ def make_template_and_spikes_objects(self): channels = self.model.clusters_channels current_probe = self.model.channel_probes[channels[t]] - channel_distance = np.sum(np.abs( - self.model.channel_positions - - self.model.channel_positions[channels[t]]), axis=1) + channel_distance = np.sum( + np.abs( + self.model.channel_positions - self.model.channel_positions[channels[t]] + ), + axis=1, + ) channel_distance[self.model.channel_probes != current_probe] += np.inf templates_inds[t, :] = np.argsort(channel_distance)[:ncw] templates[t, ...] = clusters_v[t, :][:, templates_inds[t, :]] diff --git a/phylib/io/array.py b/phylib/io/array.py index 8c3fb94..ac869a2 100644 --- a/phylib/io/array.py +++ b/phylib/io/array.py @@ -1,13 +1,11 @@ -# -*- coding: utf-8 -*- - """Utility functions for NumPy arrays.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import logging -from math import floor, ceil +from math import ceil, floor from operator import itemgetter from pathlib import Path @@ -19,9 +17,10 @@ logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Utility functions -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _clip(x, a, b): return max(a, min(b, x)) @@ -43,9 +42,7 @@ def _range_from_slice(myslice, start=None, stop=None, step=None, length=None): if length is not None: stop_inferred = floor(start + step * length) if stop is not None and stop < stop_inferred: - raise ValueError("'stop' ({stop}) and ".format(stop=stop) + - "'length' ({length}) ".format(length=length) + - "are not compatible.") + raise ValueError(f"'stop' ({stop}) and 'length' ({length}) are not compatible.") stop = stop_inferred if stop is None and length is None: raise ValueError("'stop' and 'length' cannot be both unspecified.") @@ -79,13 +76,13 @@ def _normalize(arr, keep_ratio=False): (x_min, y_min), (x_max, y_max) = arr.min(axis=0), arr.max(axis=0) if keep_ratio: - a = 1. / max(x_max - x_min, y_max - y_min) + a = 1.0 / max(x_max - x_min, y_max - y_min) ax = ay = a - bx = .5 - .5 * a * (x_max + x_min) - by = .5 - .5 * a * (y_max + y_min) + bx = 0.5 - 0.5 * a * (x_max + x_min) + by = 0.5 - 0.5 * a * (y_max + y_min) else: - ax = 1. / (x_max - x_min) - ay = 1. / (y_max - y_min) + ax = 1.0 / (x_max - x_min) + ay = 1.0 / (y_max - y_min) bx = -x_min / (x_max - x_min) by = -y_min / (y_max - y_min) @@ -137,7 +134,7 @@ def _pad(arr, n, dir='right'): """ assert dir in ('left', 'right') if n < 0: - raise ValueError("'n' must be positive: {0}.".format(n)) + raise ValueError(f"'n' must be positive: {n}.") elif n == 0: return np.zeros((0,) + arr.shape[1:], dtype=arr.dtype) n_arr = arr.shape[0] @@ -171,12 +168,10 @@ def _get_padded(data, start, end): if start < 0 and end > data.shape[0]: raise RuntimeError() if start < 0: - start_zeros = np.zeros((-start, data.shape[1]), - dtype=data.dtype) + start_zeros = np.zeros((-start, data.shape[1]), dtype=data.dtype) return np.vstack((start_zeros, data[:end])) elif end > data.shape[0]: - end_zeros = np.zeros((end - data.shape[0], data.shape[1]), - dtype=data.dtype) + end_zeros = np.zeros((end - data.shape[0], data.shape[1]), dtype=data.dtype) return np.vstack((data[start:], end_zeros)) else: return data[start:end] @@ -188,14 +183,16 @@ def _get_data_lim(arr, n_spikes=None): arr = np.abs(arr[::k]) n = arr.shape[0] arr = arr.reshape((n, -1)) - return arr.max() or 1. + return arr.max() or 1.0 def get_closest_clusters(cluster_id, cluster_ids, sim_func, max_n=None): """Return a list of pairs `(cluster, similarity)` sorted by decreasing similarity to a given cluster.""" - l = [(_as_scalar(candidate), _as_scalar(sim_func(cluster_id, candidate))) - for candidate in _as_scalars(cluster_ids)] + l = [ + (_as_scalar(candidate), _as_scalar(sim_func(cluster_id, candidate))) + for candidate in _as_scalars(cluster_ids) + ] l = sorted(l, key=itemgetter(1), reverse=True) max_n = None or len(l) return l[:max_n] @@ -209,13 +206,14 @@ def _flatten(l): # I/O functions # ----------------------------------------------------------------------------- + def read_array(path, mmap_mode=None): """Read a .npy array.""" path = Path(path) file_ext = path.suffix if file_ext == '.npy': return np.load(str(path), mmap_mode=mmap_mode) - raise NotImplementedError("The file extension `{}` is not currently supported." % file_ext) + raise NotImplementedError('The file extension `{{}}` is not currently supported.') def write_array(path, arr): @@ -224,19 +222,19 @@ def write_array(path, arr): file_ext = path.suffix if file_ext == '.npy': return np.save(str(path), arr) - raise NotImplementedError("The file extension `{}` is not currently supported." % file_ext) + raise NotImplementedError('The file extension `{{}}` is not currently supported.') # ----------------------------------------------------------------------------- # Chunking functions # ----------------------------------------------------------------------------- + def _excerpt_step(n_samples, n_excerpts=None, excerpt_size=None): """Compute the step of an excerpt set as a function of the number of excerpts or their sizes.""" assert n_excerpts >= 2 - step = max((n_samples - excerpt_size) // (n_excerpts - 1), - excerpt_size) + step = max((n_samples - excerpt_size) // (n_excerpts - 1), excerpt_size) return step @@ -299,7 +297,7 @@ def data_chunk(data, chunk, with_overlap=False): else: i, j = chunk[2:] else: - raise ValueError("'chunk' should have 2 or 4 elements, not {0:d}".format(len(chunk))) + raise ValueError(f"'chunk' should have 2 or 4 elements, not {len(chunk):d}") return data[i:j, ...] @@ -313,9 +311,12 @@ def get_excerpts(data, n_excerpts=None, excerpt_size=None): return data[:0] elif n_excerpts == 1: return data[:excerpt_size] - out = np.concatenate([ - data_chunk(data, chunk) - for chunk in excerpts(len(data), n_excerpts=n_excerpts, excerpt_size=excerpt_size)]) + out = np.concatenate( + [ + data_chunk(data, chunk) + for chunk in excerpts(len(data), n_excerpts=n_excerpts, excerpt_size=excerpt_size) + ] + ) assert len(out) <= n_excerpts * excerpt_size return out @@ -324,6 +325,7 @@ def get_excerpts(data, n_excerpts=None, excerpt_size=None): # Spike clusters utility functions # ----------------------------------------------------------------------------- + def _spikes_in_clusters(spike_clusters, clusters): """Return the ids of all spikes belonging to the specified clusters.""" if len(spike_clusters) == 0 or len(clusters) == 0: @@ -354,8 +356,9 @@ def _spikes_per_cluster(spike_clusters, spike_ids=None): # NOTE: we don't have to sort abs_spikes[...] here because the argsort # using 'mergesort' above is stable. spikes_in_clusters = { - clusters[i]: abs_spikes[idx[i]:idx[i + 1]] for i in range(len(clusters) - 1)} - spikes_in_clusters[clusters[-1]] = abs_spikes[idx[-1]:] + clusters[i]: abs_spikes[idx[i] : idx[i + 1]] for i in range(len(clusters) - 1) + } + spikes_in_clusters[clusters[-1]] = abs_spikes[idx[-1] :] return spikes_in_clusters @@ -391,24 +394,30 @@ def grouped_mean(arr, spike_clusters): # Spike selection # ----------------------------------------------------------------------------- + def _times_in_chunks(times, chunks_kept): """Return the indices of the times that belong to a list of kept chunks.""" ind = np.searchsorted(chunks_kept, times, side='right') return ind % 2 == 1 -class SpikeSelector(object): +class SpikeSelector: """Select a given number of spikes per cluster among a subset of the chunks.""" + def __init__( - self, get_spikes_per_cluster=None, spike_times=None, - chunk_bounds=None, n_chunks_kept=None): + self, + get_spikes_per_cluster=None, + spike_times=None, + chunk_bounds=None, + n_chunks_kept=None, + ): self.get_spikes_per_cluster = get_spikes_per_cluster self.spike_times = spike_times self.chunks_kept = [] n_chunks = len(chunk_bounds) - 1 for i in range(0, n_chunks, max(1, int(ceil(n_chunks / n_chunks_kept)))): - self.chunks_kept.extend(chunk_bounds[i:i + 2]) + self.chunks_kept.extend(chunk_bounds[i : i + 2]) self.chunks_kept = np.array(self.chunks_kept) def __call__(self, n_spk_clu, cluster_ids, subset_chunks=False, subset_spikes=None): diff --git a/phylib/io/datasets.py b/phylib/io/datasets.py index bc5e70d..cd6d4ab 100644 --- a/phylib/io/datasets.py +++ b/phylib/io/datasets.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- - """Utility functions for test datasets.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import hashlib import logging @@ -13,16 +11,17 @@ from phylib.utils._misc import ensure_dir_exists, phy_config_dir from phylib.utils.event import ProgressReporter - logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Utility functions -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _remote_file_size(path): import requests + try: # pragma: no cover response = requests.head(path) return int(response.headers.get('content-length', 0)) @@ -36,7 +35,7 @@ def _save_stream(r, path): size = _remote_file_size(r.url) pr = ProgressReporter() pr.value_max = size or 1 - pr.set_progress_message('Downloading `' + str(path) + '`: {progress:.1f}%.') + pr.set_progress_message(f'Downloading `{str(path)}`: {{progress:.1f}}%.') pr.set_complete_message('Download complete.') downloaded = 0 with open(path, 'wb') as f: @@ -52,9 +51,10 @@ def _save_stream(r, path): def _download(url, stream=None): from requests import get + r = get(url, stream=stream) if r.status_code != 200: # pragma: no cover - logger.debug("Error while downloading %s.", url) + logger.debug('Error while downloading %s.', url) r.raise_for_status() return r @@ -64,7 +64,7 @@ def download_text_file(url): return _download(url).text -def _md5(path, blocksize=2 ** 20): +def _md5(path, blocksize=2**20): """Compute the checksum of a file.""" m = hashlib.md5() with open(path, 'rb') as f: @@ -82,12 +82,14 @@ def _check_md5(path, checksum): def _check_md5_of_url(output_path, url): try: - checksum = download_text_file(url + '.md5').split(' ')[0] + checksum = download_text_file(f'{url}.md5').split(' ')[0] except Exception: checksum = None - finally: - if checksum: - return _check_md5(output_path, checksum) + + # Move the return outside finally + if checksum: + return _check_md5(output_path, checksum) + return None def download_file(url, output_path): @@ -110,9 +112,11 @@ def download_file(url, output_path): checked = _check_md5_of_url(output_path, url) if checked is False: logger.debug( - "The file `%s` already exists but is invalid: redownloading.", output_path) + 'The file `%s` already exists but is invalid: redownloading.', + output_path, + ) elif checked is True: - logger.debug("The file `%s` already exists: skipping.", output_path) + logger.debug('The file `%s` already exists: skipping.', output_path) return output_path r = _download(url, stream=True) _save_stream(r, output_path) @@ -121,8 +125,9 @@ def download_file(url, output_path): r = _download(url, stream=True) _save_stream(r, output_path) if _check_md5_of_url(output_path, url) is False: - raise RuntimeError("The checksum of the downloaded file " - "doesn't match the provided checksum.") + raise RuntimeError( + "The checksum of the downloaded file doesn't match the provided checksum." + ) return diff --git a/phylib/io/merge.py b/phylib/io/merge.py index a0a86c6..8254526 100644 --- a/phylib/io/merge.py +++ b/phylib/io/merge.py @@ -1,29 +1,33 @@ -# -*- coding: utf-8 -*- - """Probe merging.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import logging from pathlib import Path -from tqdm import tqdm import numpy as np from scipy.linalg import block_diag +from tqdm import tqdm -from phylib.utils._misc import ( - _read_tsv_simple, _write_tsv_simple, write_tsv, read_python, write_python) from phylib.io.model import load_model +from phylib.utils._misc import ( + _read_tsv_simple, + _write_tsv_simple, + read_python, + write_python, + write_tsv, +) logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Merge utils -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _concat(arrs, axis=0, dtype=None): dtype = dtype or arrs[0].dtype @@ -58,11 +62,12 @@ def _load_multiple_files(fn, subdirs): return [np.load(str(subdir / fn)).squeeze() for subdir in subdirs] -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Main Merger class -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ -class Merger(object): + +class Merger: """Merge spike-sorted data from different probes and output datasets to disk. Constructor @@ -80,6 +85,7 @@ class Merger(object): All fields will be saved in `probes.description.tsv`. """ + def __init__(self, subdirs, out_dir, probe_info=None): assert subdirs self.subdirs = [Path(subdir) for subdir in subdirs] @@ -93,7 +99,7 @@ def __init__(self, subdirs, out_dir, probe_info=None): def _save(self, name, arr): """Save a npy array in the output directory.""" - logger.debug("Saving %s %s %s.", name, arr.dtype, arr.shape) + logger.debug('Saving %s %s %s.', name, arr.dtype, arr.shape) np.save(self.out_dir / name, arr) def write_params(self): @@ -109,7 +115,11 @@ def write_params(self): def write_probe_desc(self): """Write the probe description in a TSV file.""" - write_tsv(self.out_dir / 'probes.description.tsv', self.probe_info, first_field='label') + write_tsv( + self.out_dir / 'probes.description.tsv', + self.probe_info, + first_field='label', + ) def write_spike_times(self): """Write the merged spike times, and register self.spike_order with the reordering @@ -133,7 +143,7 @@ def write_spike_data(self): def write_spike_clusters(self): """Write the merged spike clusters, and register self.cluster_offsets. - Write the merged spike templates, and register self.template_offsets. + Write the merged spike templates, and register self.template_offsets. """ spike_clusters_l = _load_multiple_files('spike_clusters.npy', self.subdirs) spike_templates_l = _load_multiple_files('spike_templates.npy', self.subdirs) @@ -143,7 +153,8 @@ def write_spike_clusters(self): coffset = 0 toffset = 0 for i, (subdir, sc, st) in enumerate( - zip(self.subdirs, spike_clusters_l, spike_templates_l)): + zip(self.subdirs, spike_clusters_l, spike_templates_l) + ): n_clu = np.max(sc) + 1 n_tmp = np.max(st) + 1 sc += coffset @@ -154,9 +165,11 @@ def write_spike_clusters(self): coffset += n_clu toffset += n_tmp spike_clusters = _load_multiple_spike_arrays( - *spike_clusters_l, spike_order=self.spike_order) + *spike_clusters_l, spike_order=self.spike_order + ) spike_templates = _load_multiple_spike_arrays( - *spike_templates_l, spike_order=self.spike_order) + *spike_templates_l, spike_order=self.spike_order + ) cluster_probes = _concat(cluster_probes_l) assert np.max(spike_clusters) + 1 == cluster_probes.size self._save('spike_clusters.npy', spike_clusters) @@ -165,12 +178,12 @@ def write_spike_clusters(self): def write_cluster_data(self): """We load all cluster metadata from TSV files, renumber the clusters, - merge the dictionaries, and save in a new merged TSV file. """ + merge the dictionaries, and save in a new merged TSV file.""" cluster_data = [ 'cluster_Amplitude.tsv', 'cluster_ContamPct.tsv', - 'cluster_KSLabel.tsv' + 'cluster_KSLabel.tsv', ] for fn in cluster_data: @@ -206,10 +219,10 @@ def write_channel_data(self): def write_channel_positions(self): """Write the channel positions.""" channel_positions_l = _load_multiple_files('channel_positions.npy', self.subdirs) - x_offset = 0. + x_offset = 0.0 for array in channel_positions_l: array[:, 0] += x_offset - x_offset = 2. * array[:, 0].max() - array[:, 0].min() + x_offset = 2.0 * array[:, 0].max() - array[:, 0].min() channel_positions = _concat(channel_positions_l, axis=0) self._save('channel_positions.npy', channel_positions) @@ -278,14 +291,14 @@ def write_misc(self): try: concat = block_diag(*_load_multiple_files(fn, self.subdirs)) except FileNotFoundError: - logger.debug("File %s not found, skipping.", fn) + logger.debug('File %s not found, skipping.', fn) continue self._save(fn, concat) def merge(self): """Merge the probes data and return a TemplateModel instance of the merged data.""" - with tqdm(desc="Merging", total=100) as bar: + with tqdm(desc='Merging', total=100) as bar: self.write_params() self.write_probe_desc() bar.update(10) diff --git a/phylib/io/mock.py b/phylib/io/mock.py index 54ab475..936034e 100644 --- a/phylib/io/mock.py +++ b/phylib/io/mock.py @@ -1,35 +1,33 @@ -# -*- coding: utf-8 -*- - """Mock datasets.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import numpy as np import numpy.random as nr - -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Artificial data -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def artificial_waveforms(n_spikes=None, n_samples=None, n_channels=None): - return .25 * nr.normal(size=(n_spikes, n_samples, n_channels)) + return 0.25 * nr.normal(size=(n_spikes, n_samples, n_channels)) def artificial_features(*args): - return .25 * nr.normal(size=args) + return 0.25 * nr.normal(size=args) def artificial_masks(n_spikes=None, n_channels=None): masks = nr.uniform(size=(n_spikes, n_channels)) - masks[masks < .25] = 0 + masks[masks < 0.25] = 0 return masks def artificial_traces(n_samples, n_channels): - return .25 * nr.normal(size=(n_samples, n_channels)) + return 0.25 * nr.normal(size=(n_samples, n_channels)) def artificial_spike_clusters(n_spikes, n_clusters, low=0): diff --git a/phylib/io/model.py b/phylib/io/model.py index a8aeb85..639a381 100644 --- a/phylib/io/model.py +++ b/phylib/io/model.py @@ -1,38 +1,43 @@ -# -*- coding: utf-8 -*- - """Template model.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import logging import os import os.path as op +import shutil from operator import itemgetter from pathlib import Path -import shutil import numpy as np + # from numpy.lib.format import open_memmap import scipy.io as sio -# from tqdm import tqdm -from .array import _index_of, _spikes_in_clusters, _spikes_per_cluster, SpikeSelector -from .traces import ( - get_ephys_reader, RandomEphysReader, extract_waveforms, - get_spike_waveforms, export_waveforms) from phylib.utils import Bunch -from phylib.utils._misc import _write_tsv_simple, read_tsv, read_python +from phylib.utils._misc import _write_tsv_simple, read_python, read_tsv from phylib.utils.geometry import linear_positions +# from tqdm import tqdm +from .array import SpikeSelector, _index_of, _spikes_in_clusters, _spikes_per_cluster +from .traces import ( + RandomEphysReader, + export_waveforms, + extract_waveforms, + get_ephys_reader, + get_spike_waveforms, +) + logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Utility functions -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def read_array(path, mmap_mode=None): """Read a binary array in npy or mat format, avoiding nan and inf values.""" @@ -48,11 +53,13 @@ def read_array(path, mmap_mode=None): # TODO: virtual memmap array where the replacement is done on-the-fly when reading the array. if mmap_mode is None: for w in ('nan', 'inf'): - errors = getattr(np, 'is' + w)(out) + errors = getattr(np, f'is{w}')(out) if np.any(errors): n = np.sum(errors) n_tot = errors.size - logger.warning("%d/%d values are %s in %s, replacing by zero.", n, n_tot, w, path) + logger.warning( + '%d/%d values are %s in %s, replacing by zero.', n, n_tot, w, path + ) out[errors] = 0 return out @@ -79,8 +86,9 @@ def from_sparse(data, cols, channel_ids): """ # The axis in the data that contains the channels. if len(channel_ids) != len(np.unique(channel_ids)): - raise NotImplementedError("Multiple identical requested channels " - "in from_sparse().") + raise NotImplementedError( + 'Multiple identical requested channels in from_sparse().' + ) channel_axis = 1 shape = list(data.shape) assert data.ndim >= 2 @@ -91,10 +99,15 @@ def from_sparse(data, cols, channel_ids): c = cols.flatten().astype(np.int32) # Remove columns that do not belong to the specified channels. c[~np.isin(c, channel_ids)] = -1 - assert np.all(np.isin(c, np.r_[channel_ids, -1])) + + # NumPy 2.0 compatibility fix: Create channel_ids array with sentinel value + # that works with both uint32 channel_ids and -1 sentinel + channel_ids_with_sentinel = np.concatenate([channel_ids, [-1]]) + + assert np.all(np.isin(c, channel_ids_with_sentinel)) # Convert column indices to relative indices given the specified # channel_ids. - cols_loc = _index_of(c, np.r_[channel_ids, -1]).reshape(cols.shape) + cols_loc = _index_of(c, channel_ids_with_sentinel).reshape(cols.shape) assert cols_loc.shape == (n_spikes, n_channels_loc) n_channels = len(channel_ids) # Shape of the output array. @@ -103,8 +116,7 @@ def from_sparse(data, cols, channel_ids): # The last column contains irrelevant values. out_shape[channel_axis] = n_channels + 1 out = np.zeros(out_shape, dtype=data.dtype) - x = np.tile(np.arange(n_spikes)[:, np.newaxis], - (1, n_channels_loc)) + x = np.tile(np.arange(n_spikes)[:, np.newaxis], (1, n_channels_loc)) assert x.shape == cols_loc.shape == data.shape[:2] out[x, cols_loc, ...] = data # Remove the last column with values outside the specified @@ -139,13 +151,14 @@ def save_metadata(filename, field_name, metadata): return _write_tsv_simple(filename, field_name, metadata) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Channel util functions -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _all_positions_distinct(positions): """Return whether all positions are distinct.""" - return len(set(tuple(row) for row in positions)) == len(positions) + return len({tuple(row) for row in positions}) == len(positions) def get_closest_channels(channel_positions, channel_index, n=None): @@ -161,9 +174,10 @@ def get_closest_channels(channel_positions, channel_index, n=None): return out -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # PC computation -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _compute_pcs(x, npcs): """Compute the PCs of an array x, where each row is an observation. @@ -188,7 +202,7 @@ def _compute_pcs(x, npcs): assert x_channel.ndim == 2 # Don't compute the cov matrix if there are no unmasked spikes # on that channel. - alpha = 1. / nspikes + alpha = 1.0 / nspikes if x_channel.shape[0] <= 1: cov = alpha * cov_reg else: @@ -239,9 +253,10 @@ def compute_features(waveforms): return features -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # I/O util functions -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _find_first_existing_path(*paths, multiple_ok=True): out = [] @@ -250,7 +265,7 @@ def _find_first_existing_path(*paths, multiple_ok=True): if path.exists(): out.append(path) if len(out) >= 2 and not multiple_ok: # pragma: no cover - raise IOError("Multiple conflicting files exist: %s." % ', '.join((out, path))) + raise OSError(f'Multiple conflicting files exist: {", ".join((out, path))}.') elif len(out) >= 1: return out[0] else: @@ -260,27 +275,34 @@ def _find_first_existing_path(*paths, multiple_ok=True): def _close_memmap(name, obj): """Close a memmap array or a list of memmap arrays.""" if isinstance(obj, np.memmap): - logger.debug("Close memmap array %s.", name) + logger.debug('Close memmap array %s.', name) obj._mmap.close() elif getattr(obj, 'arrs', None) is not None: # pragma: no cover # Support ConcatenatedArrays. # NOTE: no longer used since EphysTraces - _close_memmap('%s.arrs' % name, obj.arrs) + _close_memmap(f'{name}.arrs', obj.arrs) elif isinstance(obj, (list, tuple)): - [_close_memmap('%s[]' % name, item) for item in obj] + [_close_memmap(f'{name}[]', item) for item in obj] elif isinstance(obj, dict): - [_close_memmap('%s.%s' % (name, n), item) for n, item in obj.items()] + [_close_memmap(f'{name}.{n}', item) for n, item in obj.items()] -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Template model -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Special spike_*.npy files that should not be considered as "spike attributes". -SKIP_SPIKE_ATTRS = ('clusters', 'templates', 'samples', 'times', 'times_reordered', 'amplitudes') +SKIP_SPIKE_ATTRS = ( + 'clusters', + 'templates', + 'samples', + 'times', + 'times_reordered', + 'amplitudes', +) -class TemplateModel(object): +class TemplateModel: """Object holding all data of a KiloSort/phy dataset. Constructor @@ -327,30 +349,32 @@ def __init__(self, **kwargs): elif not isinstance(self.dat_path, (list, tuple)): self.dat_path = [self.dat_path] assert isinstance(self.dat_path, (list, tuple)) - self.dat_path = [Path(p).resolve() if not Path(p).is_symlink() else p for p in self.dat_path] + self.dat_path = [ + Path(p).resolve() if not Path(p).is_symlink() else p for p in self.dat_path + ] self.dtype = getattr(self, 'dtype', np.int16) if not self.sample_rate: # pragma: no cover - logger.warning("No sample rate was given! Defaulting to 1 Hz.") - self.sample_rate = float(self.sample_rate or 1.) + logger.warning('No sample rate was given! Defaulting to 1 Hz.') + self.sample_rate = float(self.sample_rate or 1.0) assert self.sample_rate > 0 self.offset = getattr(self, 'offset', 0) self._load_data() - #-------------------------------------------------------------------------- + # -------------------------------------------------------------------------- # Internal loading methods - #-------------------------------------------------------------------------- + # -------------------------------------------------------------------------- def _load_data(self): """Load all data.""" # Spikes self.spike_samples, self.spike_times = self._load_spike_samples() - ns, = self.n_spikes, = self.spike_times.shape + (ns,) = (self.n_spikes,) = self.spike_times.shape # Make sure the spike times are increasing. if not np.all(np.diff(self.spike_times) >= 0): - raise ValueError("The spike times must be increasing.") + raise ValueError('The spike times must be increasing.') # Spike amplitudes. self.amplitudes = self._load_amplitudes() @@ -387,7 +411,8 @@ def _load_data(self): assert self.channel_positions.shape == (nc, 2) if not _all_positions_distinct(self.channel_positions): # pragma: no cover logger.error( - "Some channels are on the same position, please check the channel positions file.") + 'Some channels are on the same position, please check the channel positions file.' + ) self.channel_positions = linear_positions(nc) # Channel shanks. @@ -403,18 +428,24 @@ def _load_data(self): # Templates. self.sparse_templates = self._load_templates() if self.sparse_templates is not None: - self.n_templates, self.n_samples_waveforms, self.n_channels_loc = \ + self.n_templates, self.n_samples_waveforms, self.n_channels_loc = ( self.sparse_templates.data.shape + ) if self.sparse_templates.cols is not None: - assert self.sparse_templates.cols.shape == (self.n_templates, self.n_channels_loc) + assert self.sparse_templates.cols.shape == ( + self.n_templates, + self.n_channels_loc, + ) else: # pragma: no cover self.n_templates = self.spike_templates.max() + 1 self.n_samples_waveforms = 0 self.n_channels_loc = 0 # Clusters waveforms - if not np.all(self.spike_clusters == self.spike_templates) and \ - self.sparse_templates.cols is None: + if ( + not np.all(self.spike_clusters == self.spike_templates) + and self.sparse_templates.cols is None + ): self.merge_map, self.nan_idx = self.get_merge_map() self.sparse_clusters = self.cluster_waveforms() self.n_clusters = self.spike_clusters.max() + 1 @@ -430,14 +461,14 @@ def _load_data(self): # Whitening. try: self.wm = self._load_wm() - except IOError: - logger.debug("Whitening matrix file not found.") + except OSError: + logger.debug('Whitening matrix file not found.') self.wm = np.eye(nc) assert self.wm.shape == (nc, nc) try: self.wmi = self._load_wmi() - except IOError: - logger.debug("Whitening matrix inverse file not found, computing it.") + except OSError: + logger.debug('Whitening matrix inverse file not found, computing it.') self.wmi = self._compute_wmi(self.wm) assert self.wmi.shape == (nc, nc) @@ -453,8 +484,10 @@ def _load_data(self): self.duration = self.spike_times[-1] if self.spike_times[-1] > self.duration: # pragma: no cover logger.warning( - "There are %d/%d spikes after the end of the recording.", - np.sum(self.spike_times > self.duration), self.n_spikes) + 'There are %d/%d spikes after the end of the recording.', + np.sum(self.spike_times > self.duration), + self.n_spikes, + ) # Features. self.sparse_features = self._load_features() @@ -465,7 +498,10 @@ def _load_data(self): # Template features. self.sparse_template_features = self._load_template_features() self.template_features = ( - self.sparse_template_features.data if self.sparse_template_features else None) + self.sparse_template_features.data + if self.sparse_template_features + else None + ) # Spike attributes. self.spike_attributes = self._load_spike_attributes() @@ -474,17 +510,19 @@ def _load_data(self): self.metadata = self._load_metadata() def _find_path(self, *names, multiple_ok=True, mandatory=True): - full_paths = list(l[0] for l in [list(self.dir_path.glob(name)) for name in names] if l) + full_paths = [ + l[0] for l in [list(self.dir_path.glob(name)) for name in names] if l + ] path = _find_first_existing_path(*full_paths, multiple_ok=multiple_ok) if mandatory and not path: - raise IOError( - "None of these files could be found in %s: %s." % - (self.dir_path, ', '.join(names))) + raise OSError( + f'None of these files could be found in {self.dir_path}: {", ".join(names)}.' + ) return path def _read_array(self, path, mmap_mode=None): if not path: - raise IOError() + raise OSError() return read_array(path, mmap_mode=mmap_mode).squeeze() def _write_array(self, path, arr): @@ -501,18 +539,18 @@ def _load_metadata(self): for filename in files: if filename.stem in excluded_names: continue - logger.debug("Load `%s`.", filename.name) + logger.debug('Load `%s`.', filename.name) try: for field, data in load_metadata(filename).items(): metadata[field] = data except Exception as e: - logger.warning("Error when reading %s: %s.", filename.name, str(e)) + logger.warning('Error when reading %s: %s.', filename.name, str(e)) continue return metadata - #-------------------------------------------------------------------------- + # -------------------------------------------------------------------------- # Specific loading methods - #-------------------------------------------------------------------------- + # -------------------------------------------------------------------------- def _load_spike_attributes(self): """Load all spike_*.npy files, called spike attributes.""" @@ -527,9 +565,9 @@ def _load_spike_attributes(self): try: arr = self._read_array(filename) assert arr.shape[0] == self.n_spikes - logger.debug("Load %s.", filename.name) - except (IOError, AssertionError) as e: - logger.warning("Unable to open %s: %s.", filename.name, e) + logger.debug('Load %s.', filename.name) + except (OSError, AssertionError) as e: + logger.warning('Unable to open %s: %s.', filename.name, e) continue spike_attributes[n] = arr return spike_attributes @@ -543,7 +581,9 @@ def _load_channel_map(self): return out def _load_channel_positions(self): - path = self._find_path('channel_positions.npy', 'channels.localCoordinates*.npy') + path = self._find_path( + 'channel_positions.npy', 'channels.localCoordinates*.npy' + ) out = self._read_array(path) out = np.atleast_2d(out) assert out.ndim == 2 @@ -556,7 +596,7 @@ def _load_channel_probes(self): out = np.atleast_1d(out) assert out.ndim == 1 return out - except IOError: + except OSError: return np.zeros(self.n_channels, dtype=np.int32) def _load_channel_shanks(self): @@ -565,32 +605,40 @@ def _load_channel_shanks(self): out = self._read_array(path).reshape((-1,)) assert out.ndim == 1 return out - except IOError: - logger.debug("No channel shank file found.") + except OSError: + logger.debug('No channel shank file found.') return np.zeros(self.n_channels, dtype=np.int32) def _load_traces(self, channel_map=None): if not self.dat_path: if os.environ.get('PHY_VIRTUAL_RAW_DATA', None): # pragma: no cover n_samples = int((self.spike_times[-1] + 1) * self.sample_rate) - return RandomEphysReader(n_samples, len(channel_map), sample_rate=self.sample_rate) + return RandomEphysReader( + n_samples, len(channel_map), sample_rate=self.sample_rate + ) return n = self.n_channels_dat # self.dat_path could be any object accepted by get_ephys_reader(). traces = get_ephys_reader( - self.dat_path, n_channels_dat=n, dtype=self.dtype, offset=self.offset, - sample_rate=self.sample_rate) + self.dat_path, + n_channels_dat=n, + dtype=self.dtype, + offset=self.offset, + sample_rate=self.sample_rate, + ) if traces is not None: traces = traces[:, channel_map] # lazy permutation on the channel axis return traces def _load_amplitudes(self): try: - out = self._read_array(self._find_path('amplitudes.npy', 'spikes.amps*.npy')) + out = self._read_array( + self._find_path('amplitudes.npy', 'spikes.amps*.npy') + ) assert out.ndim == 1 return out - except IOError: - logger.debug("No amplitude file found.") + except OSError: + logger.debug('No amplitude file found.') return def _load_spike_templates(self): @@ -601,29 +649,35 @@ def _load_spike_templates(self): uc = np.unique(out) if np.max(uc) - np.min(uc) + 1 != uc.size: logger.warning( - "Unreferenced clusters found in templates (generally not a problem)") + 'Unreferenced clusters found in templates (generally not a problem)' + ) assert out.dtype in (np.uint16, np.uint32, np.int32, np.int64) assert out.ndim == 1 return out def _load_spike_clusters(self): path = self._find_path( - 'spike_clusters.npy', 'spikes.clusters*.npy', multiple_ok=False, mandatory=False) + 'spike_clusters.npy', + 'spikes.clusters*.npy', + multiple_ok=False, + mandatory=False, + ) if path is None: # Create spike_clusters file if it doesn't exist. tmp_path = self._find_path('spike_templates.npy', 'spikes.clusters*.npy') path = self.dir_path / 'spike_clusters.npy' - logger.debug("Copying from %s to %s.", tmp_path, path) + logger.debug('Copying from %s to %s.', tmp_path, path) shutil.copy(tmp_path, path) assert path.exists() - logger.debug("Loading spike clusters.") + logger.debug('Loading spike clusters.') # NOTE: we make a copy in memory so that we can update this array # during manual clustering. out = self._read_array(path).astype(np.int32) uc = np.unique(out) if np.max(uc) - np.min(uc) + 1 != uc.size: logger.warning( - "Unreferenced clusters found in spike_clusters (generally not a problem)") + 'Unreferenced clusters found in spike_clusters (generally not a problem)' + ) assert out.ndim == 1 return out @@ -632,7 +686,7 @@ def _load_spike_reorder(self): samples (not in seconds).""" path = self.dir_path / 'spike_times_reordered.npy' if path.exists(): - logger.debug("Loading spike times reordered.") + logger.debug('Loading spike times reordered.') samples = self._read_array(path).squeeze() times = samples / self.sample_rate assert times.shape == (self.n_spikes,) @@ -654,7 +708,9 @@ def _load_spike_samples(self): if samples_path: samples = self._read_array(samples_path) else: - logger.info("Loading spikes.times.npy in seconds, converting to samples.") + logger.info( + 'Loading spikes.times.npy in seconds, converting to samples.' + ) samples = np.round(times * self.sample_rate).astype(np.uint64) assert samples.ndim == times.ndim == 1 return samples, times @@ -665,10 +721,13 @@ def _load_spike_waveforms(self): # pragma: no cover path_spikes = self.dir_path / '_phy_spikes_subset.spikes.npy' if not path.exists() or not path_channels.exists() or not path_spikes.exists(): logger.warning( - "Skipping spike waveforms that do not exist, they will be extracted " - "on the fly from the raw data as needed.") + 'Skipping spike waveforms that do not exist, they will be extracted ' + 'on the fly from the raw data as needed.' + ) return - logger.debug("Loading spikes subset waveforms to avoid fetching waveforms from raw data.") + logger.debug( + 'Loading spikes subset waveforms to avoid fetching waveforms from raw data.' + ) try: return Bunch( waveforms=self._read_array(path, mmap_mode='r'), @@ -676,7 +735,7 @@ def _load_spike_waveforms(self): # pragma: no cover spike_ids=self._read_array(path_spikes), ) except Exception as e: - logger.warning("Could not load spike waveforms: %s.", e) + logger.warning('Could not load spike waveforms: %s.', e) return def _load_similar_templates(self): @@ -685,16 +744,17 @@ def _load_similar_templates(self): out = np.atleast_2d(out) assert out.ndim == 2 return out - except IOError: + except OSError: return np.zeros((self.n_templates, self.n_templates)) def _load_templates(self): - logger.debug("Loading templates.") + logger.debug('Loading templates.') # Sparse structure: regular array with col indices. try: path = self._find_path( - 'templates.npy', 'templates.waveforms.npy', 'templates.waveforms.*.npy') + 'templates.npy', 'templates.waveforms.npy', 'templates.waveforms.*.npy' + ) data = self._read_array(path, mmap_mode='r+') data = np.atleast_3d(data) assert data.ndim == 3 @@ -703,7 +763,7 @@ def _load_templates(self): empty_templates = np.all(np.all(np.isnan(data), axis=1), axis=1) data[empty_templates, ...] = 0 n_templates, n_samples, n_channels_loc = data.shape - except IOError: + except OSError: return try: @@ -712,41 +772,43 @@ def _load_templates(self): # That means templates.npy is considered as a dense array. # Proper fix would be to save templates.npy as a true sparse array, with proper # template_ind.npy (without an s). - path = self._find_path('template_ind.npy', 'templates.waveformsChannels*.npy') + path = self._find_path( + 'template_ind.npy', 'templates.waveformsChannels*.npy' + ) cols = self._read_array(path) if cols.ndim != 2: # pragma: no cover cols = np.atleast_2d(cols).T assert cols.ndim == 2 - logger.debug("Templates are sparse.") + logger.debug('Templates are sparse.') assert cols.shape == (n_templates, n_channels_loc) - except IOError: - logger.debug("Templates are dense.") + except OSError: + logger.debug('Templates are dense.') cols = None return Bunch(data=data, cols=cols) def _load_wm(self): - logger.debug("Loading the whitening matrix.") + logger.debug('Loading the whitening matrix.') out = self._read_array(self._find_path('whitening_mat.npy')) out = np.atleast_2d(out) assert out.ndim == 2 return out def _load_wmi(self): # pragma: no cover - logger.debug("Loading the inverse of the whitening matrix.") + logger.debug('Loading the inverse of the whitening matrix.') out = self._read_array(self._find_path('whitening_mat_inv.npy')) out = np.atleast_2d(out) assert out.ndim == 2 return out def _compute_wmi(self, wm): - logger.debug("Inversing the whitening matrix %s.", wm.shape) + logger.debug('Inversing the whitening matrix %s.', wm.shape) try: wmi = np.linalg.inv(wm) except np.linalg.LinAlgError as e: # pragma: no cover - raise ValueError( - "Error when inverting the whitening matrix: %s.", e) + raise ValueError(f'Error when inverting the whitening matrix: {e}.') from e + # ^^^^^^^ self._write_array(self.dir_path / 'whitening_mat_inv.npy', wmi) return wmi @@ -760,12 +822,10 @@ def _unwhiten(self, x, channel_ids=None): return np.ascontiguousarray(out) def _load_features(self): - # Sparse structure: regular array with row and col indices. try: - logger.debug("Loading features.") - data = self._read_array( - self._find_path('pc_features.npy'), mmap_mode='r') + logger.debug('Loading features.') + data = self._read_array(self._find_path('pc_features.npy'), mmap_mode='r') if data.ndim == 2: # pragma: no cover # Deal with npcs = 1. data = data.reshape(data.shape + (1,)) @@ -773,78 +833,85 @@ def _load_features(self): assert data.dtype in (np.float32, np.float64) data = data.transpose((0, 2, 1)) n_spikes, n_channels_loc, n_pcs = data.shape - except IOError: + except OSError: return try: - cols = self._read_array(self._find_path('pc_feature_ind.npy'), mmap_mode='r') - logger.debug("Features are sparse.") + cols = self._read_array( + self._find_path('pc_feature_ind.npy'), mmap_mode='r' + ) + logger.debug('Features are sparse.') if cols.ndim == 1: # pragma: no cover # Deal with npcs = 1. cols = cols.reshape(cols.shape + (1,)) assert cols.ndim == 2 assert cols.shape == (self.n_templates, n_channels_loc) - except IOError: - logger.debug("Features are dense.") + except OSError: + logger.debug('Features are dense.') cols = None try: rows = self._read_array(self._find_path('pc_feature_spike_ids.npy')) assert rows.shape == (n_spikes,) - except IOError: + except OSError: rows = None return Bunch(data=data, cols=cols, rows=rows) def _load_template_features(self): - # Sparse structure: regular array with row and col indices. try: - logger.debug("Loading template features.") - data = self._read_array(self._find_path('template_features.npy'), mmap_mode='r') + logger.debug('Loading template features.') + data = self._read_array( + self._find_path('template_features.npy'), mmap_mode='r' + ) assert data.dtype in (np.float32, np.float64) assert data.ndim == 2 n_spikes, n_channels_loc = data.shape - except IOError: + except OSError: return try: cols = self._read_array(self._find_path('template_feature_ind.npy')) - logger.debug("Template features are sparse.") + logger.debug('Template features are sparse.') assert cols.shape == (self.n_templates, n_channels_loc) - except IOError: + except OSError: cols = None - logger.debug("Template features are dense.") + logger.debug('Template features are dense.') try: rows = self._read_array(self._find_path('template_feature_spike_ids.npy')) assert rows.shape == (n_spikes,) - except IOError: + except OSError: rows = None return Bunch(data=data, cols=cols, rows=rows) - #-------------------------------------------------------------------------- + # -------------------------------------------------------------------------- # Internal data access methods - #-------------------------------------------------------------------------- + # -------------------------------------------------------------------------- def _find_best_channels(self, template, amplitude_threshold=None): """Find the best channels for a given template.""" # Compute the template amplitude on each channel. assert template.ndim == 2 # shape: (n_samples, n_channels) amplitude = template.max(axis=0) - template.min(axis=0) - assert not np.all(np.isnan(amplitude)), "Template is all NaN!" + assert not np.all(np.isnan(amplitude)), 'Template is all NaN!' assert amplitude.ndim == 1 # shape: (n_channels,) # Find the peak channel. best_channel = np.argmax(amplitude) max_amp = amplitude[best_channel] # Find the channels X% peak. amplitude_threshold = ( - amplitude_threshold if amplitude_threshold is not None else self.amplitude_threshold) + amplitude_threshold + if amplitude_threshold is not None + else self.amplitude_threshold + ) peak_channels = np.nonzero(amplitude >= amplitude_threshold * max_amp)[0] # Find N closest channels. close_channels = get_closest_channels( - self.channel_positions, best_channel, self.n_closest_channels) + self.channel_positions, best_channel, self.n_closest_channels + ) assert best_channel in close_channels # Restrict to the channels belonging to the best channel's shank. if self.channel_shanks is not None: @@ -873,16 +940,20 @@ def _template_n_channels(self, template_id, n_channels): channel_ids += [-1] * (n_channels - len(channel_ids)) return channel_ids - def _get_template_dense(self, template_id, channel_ids=None, amplitude_threshold=None, - unwhiten=True): + def _get_template_dense( + self, template_id, channel_ids=None, amplitude_threshold=None, unwhiten=True + ): """Return data for one template.""" if not self.sparse_templates: return template_w = self.sparse_templates.data[template_id, ...] - template = self._unwhiten(template_w).astype(np.float32) if unwhiten else template_w + template = ( + self._unwhiten(template_w).astype(np.float32) if unwhiten else template_w + ) assert template.ndim == 2 channel_ids_, amplitude, best_channel = self._find_best_channels( - template, amplitude_threshold=amplitude_threshold) + template, amplitude_threshold=amplitude_threshold + ) channel_ids = channel_ids if channel_ids is not None else channel_ids_ template = template[:, channel_ids] assert template.ndim == 2 @@ -915,7 +986,11 @@ def _get_template_sparse(self, template_id, unwhiten=True): channel_ids = channel_ids.astype(np.uint32) # Unwhiten. - template = self._unwhiten(template_w, channel_ids=channel_ids) if unwhiten else template_w + template = ( + self._unwhiten(template_w, channel_ids=channel_ids) + if unwhiten + else template_w + ) template = template.astype(np.float32) assert template.ndim == 2 assert template.shape[1] == len(channel_ids) @@ -934,8 +1009,10 @@ def _get_template_sparse(self, template_id, unwhiten=True): return out def get_merge_map(self): - """"Gets the maps of merges and splits between spikes.clusters and spikes.templates""" - inverse_mapping_dict = {key: [] for key in range(np.max(self.spike_clusters) + 1)} + """ "Gets the maps of merges and splits between spikes.clusters and spikes.templates""" + inverse_mapping_dict = { + key: [] for key in range(np.max(self.spike_clusters) + 1) + } for temp in np.unique(self.spike_templates): idx = np.where(self.spike_templates == temp)[0] new_idx = self.spike_clusters[idx] @@ -943,22 +1020,29 @@ def get_merge_map(self): for n in mapping: inverse_mapping_dict[n].append(temp) - nan_idx = np.array([idx for idx, val in inverse_mapping_dict.items() if len(val) == 0]) + nan_idx = np.array( + [idx for idx, val in inverse_mapping_dict.items() if len(val) == 0] + ) return inverse_mapping_dict, nan_idx - #-------------------------------------------------------------------------- + # -------------------------------------------------------------------------- # Data access methods - #-------------------------------------------------------------------------- + # -------------------------------------------------------------------------- - def get_template(self, template_id, channel_ids=None, amplitude_threshold=None, unwhiten=True): + def get_template( + self, template_id, channel_ids=None, amplitude_threshold=None, unwhiten=True + ): """Get data about a template.""" if self.sparse_templates and self.sparse_templates.cols is not None: return self._get_template_sparse(template_id, unwhiten=unwhiten) else: return self._get_template_dense( - template_id, channel_ids=channel_ids, amplitude_threshold=amplitude_threshold, - unwhiten=unwhiten) + template_id, + channel_ids=channel_ids, + amplitude_threshold=amplitude_threshold, + unwhiten=unwhiten, + ) def get_waveforms(self, spike_ids, channel_ids=None): """Return spike waveforms on specified channels.""" @@ -972,19 +1056,25 @@ def get_waveforms(self, spike_ids, channel_ids=None): # Load from precomputed spikes. try: return get_spike_waveforms( - spike_ids, channel_ids, spike_waveforms=self.spike_waveforms, - n_samples_waveforms=nsw) + spike_ids, + channel_ids, + spike_waveforms=self.spike_waveforms, + n_samples_waveforms=nsw, + ) except AssertionError: - logger.warning( - "Error when loading waveforms from precomputed waveforms, trying to load the raw data.") - spike_samples = self.spike_samples[spike_ids] - return extract_waveforms( - self.traces, spike_samples, channel_ids, n_samples_waveforms=nsw) + logger.warning( + 'Error when loading waveforms from precomputed waveforms, trying to load the raw data.' + ) + spike_samples = self.spike_samples[spike_ids] + return extract_waveforms( + self.traces, spike_samples, channel_ids, n_samples_waveforms=nsw + ) else: # Or load directly from raw data (slower). spike_samples = self.spike_samples[spike_ids] return extract_waveforms( - self.traces, spike_samples, channel_ids, n_samples_waveforms=nsw) + self.traces, spike_samples, channel_ids, n_samples_waveforms=nsw + ) def get_features(self, spike_ids, channel_ids): """Return sparse features for given spikes.""" @@ -1062,7 +1152,9 @@ def get_template_features(self, spike_ids): cols = tf.cols[self.spike_templates[spike_ids]] else: cols = np.tile(np.arange(n_templates_loc), (len(spike_ids), 1)) - template_features = from_sparse(template_features, cols, np.arange(self.n_templates)) + template_features = from_sparse( + template_features, cols, np.arange(self.n_templates) + ) assert template_features.shape[0] == ns return template_features @@ -1075,37 +1167,45 @@ def get_depths(self): c = 0 spikes_depths = np.zeros_like(self.spike_times) * np.nan nspi = spikes_depths.shape[0] - if self.sparse_features is None or self.sparse_features.data.shape[0] != self.n_spikes: + if ( + self.sparse_features is None + or self.sparse_features.data.shape[0] != self.n_spikes + ): return None while True: ispi = np.arange(c, min(c + nbatch, nspi)) # take only first component features = self.sparse_features.data[ispi, :, 0] - features = np.maximum(features, 0) ** 2 # takes only positive values into account - ichannels = self.sparse_features.cols[self.spike_templates[ispi]].astype(np.uint32) + features = ( + np.maximum(features, 0) ** 2 + ) # takes only positive values into account + ichannels = self.sparse_features.cols[self.spike_templates[ispi]].astype( + np.uint32 + ) # features = np.square(self.sparse_features.data[ispi, :, 0]) # ichannels = self.sparse_features.cols[self.spike_templates[ispi]].astype(np.int64) ypos = self.channel_positions[ichannels, 1] with np.errstate(divide='ignore'): - spikes_depths[ispi] = (np.sum(np.transpose(ypos * features) / - np.sum(features, axis=1), axis=0)) + spikes_depths[ispi] = np.sum( + np.transpose(ypos * features) / np.sum(features, axis=1), axis=0 + ) c += nbatch if c >= nspi: break return spikes_depths - def get_amplitudes_true(self, sample2unit=1., use='templates'): + def get_amplitudes_true(self, sample2unit=1.0, use='templates'): """Convert spike amplitude values to input amplitudes units - via scaling by unwhitened template waveform. - :param sample2unit float: factor to convert the raw data to a physical unit (defaults 1.) - :returns: spike_amplitudes_volts: np.array [nspikes] spike amplitudes in raw data units - :returns: templates_volts: np.array[ntemplates, nsamples, nchannels]: templates + via scaling by unwhitened template waveform. + :param sample2unit float: factor to convert the raw data to a physical unit (defaults 1.) + :returns: spike_amplitudes_volts: np.array [nspikes] spike amplitudes in raw data units + :returns: templates_volts: np.array[ntemplates, nsamples, nchannels]: templates + in raw data units + :returns: template_amps_volts: np.array[ntemplates]: average templates amplitudes in raw data units - :returns: template_amps_volts: np.array[ntemplates]: average templates amplitudes - in raw data units - To scale the template for template matching, - raw_data_volts = templates_volts * spike_amplitudes_volts / template_amps_volts - """ + To scale the template for template matching, + raw_data_volts = templates_volts * spike_amplitudes_volts / template_amps_volts + """ # spike_amp = ks2_spike_amps * maxmin(inv_whitening(ks2_template_amps)) # to rescale the template, @@ -1127,7 +1227,9 @@ def get_amplitudes_true(self, sample2unit=1., use='templates'): templates_wfs[n, :, :] = np.matmul(sparse.data[n, :, :], self.wmi) # The amplitude on each channel is the positive peak minus the negative - templates_ch_amps = np.max(templates_wfs, axis=1) - np.min(templates_wfs, axis=1) + templates_ch_amps = np.max(templates_wfs, axis=1) - np.min( + templates_wfs, axis=1 + ) # The template arbitrary unit amplitude is the amplitude of its largest channel # (but see below for true tempAmps) @@ -1136,19 +1238,24 @@ def get_amplitudes_true(self, sample2unit=1., use='templates'): with np.errstate(divide='ignore', invalid='ignore'): # take the average spike amplitude per template - templates_amps_v = (np.bincount(spikes, weights=spike_amps) / - np.bincount(spikes)) + templates_amps_v = np.bincount(spikes, weights=spike_amps) / np.bincount( + spikes + ) # scale back the template according to the spikes units - templates_physical_unit = templates_wfs * (templates_amps_v / templates_amps_au - )[:, np.newaxis, np.newaxis] + templates_physical_unit = ( + templates_wfs + * (templates_amps_v / templates_amps_au)[:, np.newaxis, np.newaxis] + ) - return (spike_amps * sample2unit, - templates_physical_unit * sample2unit, - templates_amps_v * sample2unit) + return ( + spike_amps * sample2unit, + templates_physical_unit * sample2unit, + templates_amps_v * sample2unit, + ) - #-------------------------------------------------------------------------- + # -------------------------------------------------------------------------- # Internal helper methods for public high-level methods - #-------------------------------------------------------------------------- + # -------------------------------------------------------------------------- def _get_template_from_spikes(self, spike_ids): """Get the main template from a set of spikes.""" @@ -1163,24 +1270,25 @@ def _get_template_from_spikes(self, spike_ids): # We return the template. return template - #-------------------------------------------------------------------------- + # -------------------------------------------------------------------------- # Public high-level methods - #-------------------------------------------------------------------------- + # -------------------------------------------------------------------------- def describe(self): """Display basic information about the dataset.""" + def _print(name, value): - print("{0: <24}{1}".format(name, value)) + print(f'{name: <24}{value}') _print('Data files', ', '.join(map(str, self.dat_path))) _print('Directory', self.dir_path) - _print('Duration', '{:.1f}s'.format(self.duration)) - _print('Sample rate', '{:.1f} kHz'.format(self.sample_rate / 1000.)) + _print('Duration', f'{self.duration:.1f}s') + _print('Sample rate', f'{self.sample_rate / 1000.0:.1f} kHz') _print('Data type', self.dtype) _print('# of channels', self.n_channels) _print('# of channels (raw)', self.n_channels_dat) _print('# of templates', self.n_templates) - _print('# of spikes', "{:,}".format(self.n_spikes)) + _print('# of spikes', f'{self.n_spikes:,}') def get_template_counts(self, cluster_id): """Return a histogram of the number of spikes in each template for a given cluster.""" @@ -1222,8 +1330,10 @@ def get_cluster_mean_waveforms(self, cluster_id, unwhiten=True): template = self.get_template(best_template, unwhiten=unwhiten) channel_ids = template.channel_ids # Get all templates from which this cluster stems from. - templates = [self.get_template(template_id, unwhiten=unwhiten) - for template_id in template_ids] + templates = [ + self.get_template(template_id, unwhiten=unwhiten) + for template_id in template_ids + ] # Construct the waveforms array. ns = self.n_samples_waveforms data = np.zeros((len(template_ids), ns, self.n_channels)) @@ -1259,11 +1369,13 @@ def clusters_channels(self): return channels def _channels(self, sparse): - """ Gets peak channels for each waveform""" + """Gets peak channels for each waveform""" tmp = sparse.data n_templates, n_samples, n_channels = tmp.shape if sparse.cols is None: - template_peak_channels = np.argmax(tmp.max(axis=1) - tmp.min(axis=1), axis=1) + template_peak_channels = np.argmax( + tmp.max(axis=1) - tmp.min(axis=1), axis=1 + ) else: # when the templates are sparse, the first channel is the highest amplitude channel template_peak_channels = sparse.cols[:, 0] @@ -1286,7 +1398,7 @@ def clusters_amplitudes(self): return self._amplitudes(self.spike_clusters) def _amplitudes(self, tmp): - """ Compute average amplitude for spikes""" + """Compute average amplitude for spikes""" tid = np.unique(tmp) n = np.bincount(tmp)[tid] a = np.bincount(tmp, weights=self.amplitudes)[tid] @@ -1309,8 +1421,12 @@ def _waveform_durations(self, tmp): # Compute the peak channels for each template. template_peak_channels = np.argmax(tmp.max(axis=1) - tmp.min(axis=1), axis=1) durations = tmp.argmax(axis=1) - tmp.argmin(axis=1) - ind = np.ravel_multi_index((np.arange(0, n_templates), template_peak_channels), - (n_templates, n_channels), mode='raise', order='C') + ind = np.ravel_multi_index( + (np.arange(0, n_templates), template_peak_channels), + (n_templates, n_channels), + mode='raise', + order='C', + ) return durations.flatten()[ind].astype(np.float64) / self.sample_rate * 1e3 def cluster_waveforms(self): @@ -1324,37 +1440,41 @@ def cluster_waveforms(self): for clust, val in self.merge_map.items(): if len(val) > 1: mean_waveform = self.get_cluster_mean_waveforms(clust, unwhiten=False) - data[clust, :, mean_waveform.channel_ids] = \ - np.swapaxes(mean_waveform.mean_waveforms, 0, 1) + data[clust, :, mean_waveform.channel_ids] = np.swapaxes( + mean_waveform.mean_waveforms, 0, 1 + ) elif len(val) == 1: data[clust, :, :] = self.sparse_templates.data[val[0], :, :] return Bunch(data=data, cols=None) - #-------------------------------------------------------------------------- + # -------------------------------------------------------------------------- # Saving methods - #-------------------------------------------------------------------------- + # -------------------------------------------------------------------------- def save_metadata(self, name, values): """Save a dictionary {cluster_id: value} with cluster metadata in a TSV file.""" - path = self.dir_path / ('cluster_%s.tsv' % name) - logger.debug("Save cluster metadata to `%s`.", path) + path = self.dir_path / (f'cluster_{name}.tsv') + logger.debug('Save cluster metadata to `%s`.', path) # Remove empty values. - save_metadata( - path, name, {c: v for c, v in values.items() if v is not None}) + save_metadata(path, name, {c: v for c, v in values.items() if v is not None}) def save_spike_clusters(self, spike_clusters): """Save the spike clusters.""" - path = self._find_path('spike_clusters.npy', 'spikes.clusters.npy', multiple_ok=False) - logger.debug("Save spike clusters to `%s`.", path) + path = self._find_path( + 'spike_clusters.npy', 'spikes.clusters.npy', multiple_ok=False + ) + logger.debug('Save spike clusters to `%s`.', path) np.save(path, spike_clusters) - def save_spikes_subset_waveforms(self, max_n_spikes_per_template=None, max_n_channels=None, - sample2unit=1.): + def save_spikes_subset_waveforms( + self, max_n_spikes_per_template=None, max_n_channels=None, sample2unit=1.0 + ): if self.traces is None: logger.warning( - "Spike waveforms could not be extracted as the raw data file is not available.") + 'Spike waveforms could not be extracted as the raw data file is not available.' + ) return n_chunks_kept = 20 # TODO: better choice @@ -1374,29 +1494,37 @@ def save_spikes_subset_waveforms(self, max_n_spikes_per_template=None, max_n_cha template_ids = sorted(spt.keys()) ss = SpikeSelector( get_spikes_per_cluster=lambda cl: spt.get(cl, np.array([], dtype=np.int64)), - spike_times=self.spike_samples, chunk_bounds=self.traces.chunk_bounds, - n_chunks_kept=n_chunks_kept) + spike_times=self.spike_samples, + chunk_bounds=self.traces.chunk_bounds, + n_chunks_kept=n_chunks_kept, + ) spike_ids = ss(max_n_spikes_per_template, template_ids, subset_chunks=True) # Save the spike ids. ns = len(spike_ids) - logger.debug("Saving spike waveforms: %d spikes.", ns) + logger.debug('Saving spike waveforms: %d spikes.', ns) np.save(path_spikes, spike_ids) # Save the spike channels. - best_channels = np.vstack([ - self._template_n_channels(t, nc) for t in range(self.n_templates)]).astype(np.int32) + best_channels = np.vstack( + [self._template_n_channels(t, nc) for t in range(self.n_templates)] + ).astype(np.int32) assert best_channels.ndim == 2 assert best_channels.shape[0] == self.n_templates spike_channels = best_channels[self.spike_templates[spike_ids], :] assert spike_channels.shape == (ns, nc) - logger.debug("Saving spike waveforms: spike channels.") + logger.debug('Saving spike waveforms: spike channels.') np.save(path_channels, spike_channels) # Extract waveforms from the raw data on a chunk by chunk basis. export_waveforms( - path, self.traces, self.spike_samples[spike_ids], spike_channels, - n_samples_waveforms=self.n_samples_waveforms, sample2unit=sample2unit) + path, + self.traces, + self.spike_samples[spike_ids], + spike_channels, + n_samples_waveforms=self.n_samples_waveforms, + sample2unit=sample2unit, + ) # Reload spike waveforms. self.spike_waveforms = self._load_spike_waveforms() @@ -1412,7 +1540,7 @@ def _make_abs_path(p, dir_path): if not op.isabs(p): p = dir_path / p if not p.exists(): - logger.warning("File %s does not exist.", p) + logger.warning('File %s does not exist.', p) return p @@ -1431,7 +1559,9 @@ def get_template_params(params_path): if isinstance(params['dat_path'], str): params['dat_path'] = [params['dat_path']] - params['dat_path'] = [_make_abs_path(_, params['dir_path']) for _ in params['dat_path']] + params['dat_path'] = [ + _make_abs_path(_, params['dir_path']) for _ in params['dat_path'] + ] return params diff --git a/phylib/io/tests/conftest.py b/phylib/io/tests/conftest.py index 7badbf1..b62ad11 100644 --- a/phylib/io/tests/conftest.py +++ b/phylib/io/tests/conftest.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- - """Test fixtures.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import logging import shutil @@ -12,16 +10,17 @@ import numpy as np from pytest import fixture +from phylib.io.datasets import download_test_file from phylib.utils._misc import write_text, write_tsv + from ..model import load_model, write_array -from phylib.io.datasets import download_test_file logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Fixtures -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ _FILES = [ 'template/params.py', @@ -30,23 +29,17 @@ 'template/spike_templates.npy', 'template/spike_clusters.npy', 'template/amplitudes.npy', - 'template/cluster_group.tsv', - 'template/channel_map.npy', 'template/channel_positions.npy', 'template/channel_shanks.npy', - 'template/similar_templates.npy', 'template/whitening_mat.npy', - 'template/templates.npy', 'template/template_ind.npy', - 'template/pc_features.npy', 'template/pc_feature_ind.npy', 'template/pc_feature_spike_ids.npy', - 'template/template_features.npy', 'template/template_feature_ind.npy', 'template/template_feature_spike_ids.npy', @@ -56,7 +49,7 @@ def _remove(path): if path.exists(): path.unlink() - logger.debug("Removed %s.", path) + logger.debug('Removed %s.', path) def _make_dataset(tempdir, param='dense', has_spike_attributes=True): @@ -70,7 +63,7 @@ def _make_dataset(tempdir, param='dense', has_spike_attributes=True): # Skip sparse arrays if is_sparse is False. if param == 'sparse' and ('_ind.' in str(to_path) or 'spike_ids.' in str(to_path)): continue - logger.debug("Copying file to %s.", to_path) + logger.debug('Copying file to %s.', to_path) shutil.copy(path, to_path) # Some changes to files if 'misc' fixture parameter. @@ -84,7 +77,7 @@ def _make_dataset(tempdir, param='dense', has_spike_attributes=True): assert st_r.shape == st.shape # Reordered spikes. np.save(tempdir / 'spike_times_reordered.npy', st_r) - np.save(tempdir / 'spikes.times.npy', st / 25000.) # sample rate + np.save(tempdir / 'spikes.times.npy', st / 25000.0) # sample rate _remove(tempdir / 'spike_times.npy') # Buggy TSV file should not cause a crash. write_text(tempdir / 'error.tsv', '') @@ -119,15 +112,19 @@ def _make_dataset(tempdir, param='dense', has_spike_attributes=True): # TSV file with cluster data. write_tsv( - tempdir / 'cluster_Amplitude.tsv', [{'cluster_id': 1, 'Amplitude': 123.4}], - first_field='cluster_id') + tempdir / 'cluster_Amplitude.tsv', + [{'cluster_id': 1, 'Amplitude': 123.4}], + first_field='cluster_id', + ) write_tsv( - tempdir / 'cluster_metrics.tsv', [ + tempdir / 'cluster_metrics.tsv', + [ {'cluster_id': 2, 'met1': 123.4, 'met2': 'hello world 1'}, {'cluster_id': 3, 'met1': 5.678}, {'cluster_id': 5, 'met2': 'hello world 2'}, - ]) + ], + ) template_path = tempdir / paths[0].name return template_path diff --git a/phylib/io/tests/test_alf.py b/phylib/io/tests/test_alf.py index cbd4428..4ab086a 100644 --- a/phylib/io/tests/test_alf.py +++ b/phylib/io/tests/test_alf.py @@ -1,30 +1,29 @@ -# -*- coding: utf-8 -*- - """Test ALF dataset generation.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import os -from pathlib import Path import shutil -from pytest import fixture, raises +from pathlib import Path import numpy as np import numpy.random as nr +from pytest import fixture, raises from phylib.utils._misc import _write_tsv_simple -from ..alf import _FILE_RENAMES, _load, EphysAlfCreator -from ..model import TemplateModel +from ..alf import _FILE_RENAMES, EphysAlfCreator, _load +from ..model import TemplateModel -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Fixture -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ -class Dataset(object): + +class Dataset: def __init__(self, tempdir): np.random.seed(42) self.tmp_dir = tempdir @@ -35,7 +34,7 @@ def __init__(self, tempdir): self.nc = 10 self.nt = 5 self.ncd = 1000 - np.save(p / 'spike_times.npy', .01 * np.cumsum(nr.exponential(size=self.ns))) + np.save(p / 'spike_times.npy', 0.01 * np.cumsum(nr.exponential(size=self.ns))) np.save(p / 'spike_clusters.npy', nr.randint(low=1, high=self.nt, size=self.ns)) shutil.copy(p / 'spike_clusters.npy', p / 'spike_templates.npy') np.save(p / 'amplitudes.npy', nr.uniform(low=0.5, high=1.5, size=self.ns)) @@ -47,13 +46,17 @@ def __init__(self, tempdir): np.save(p / 'whitening_mat.npy', np.eye(self.nc, self.nc)) np.save(p / '_phy_spikes_subset.channels.npy', np.zeros([self.ns, self.ncmax])) np.save(p / '_phy_spikes_subset.spikes.npy', np.zeros([self.ns])) - np.save(p / '_phy_spikes_subset.waveforms.npy', np.zeros( - [self.ns, self.nsamp, self.ncmax]) + np.save( + p / '_phy_spikes_subset.waveforms.npy', + np.zeros([self.ns, self.nsamp, self.ncmax]), ) _write_tsv_simple(p / 'cluster_group.tsv', 'group', {2: 'good', 3: 'mua', 5: 'noise'}) - _write_tsv_simple(p / 'cluster_Amplitude.tsv', field_name='Amplitude', - data={str(n): np.random.rand() * 120 for n in np.arange(self.nt)}) + _write_tsv_simple( + p / 'cluster_Amplitude.tsv', + field_name='Amplitude', + data={str(n): np.random.rand() * 120 for n in np.arange(self.nt)}, + ) with open(p / 'probes.description.txt', 'w+') as fid: fid.writelines(['label\n']) @@ -90,7 +93,10 @@ def test_ephys_1(dataset): assert dataset._load('rawdata.npy').shape == (1000, dataset.nc) assert dataset._load('mydata.lf.bin').shape == (1000 * dataset.nc,) assert dataset._load('whitening_mat.npy').shape == (dataset.nc, dataset.nc) - assert dataset._load('_phy_spikes_subset.channels.npy').shape == (dataset.ns, dataset.ncmax) + assert dataset._load('_phy_spikes_subset.channels.npy').shape == ( + dataset.ns, + dataset.ncmax, + ) assert dataset._load('_phy_spikes_subset.spikes.npy').shape == (dataset.ns,) assert dataset._load('_phy_spikes_subset.waveforms.npy').shape == ( (dataset.ns, dataset.nsamp, dataset.ncmax) @@ -102,7 +108,11 @@ def test_spike_depths(dataset): out_path = path / 'alf' mtemp = TemplateModel( - dir_path=path, dat_path=dataset.dat_path, sample_rate=2000, n_channels_dat=dataset.nc) + dir_path=path, + dat_path=dataset.dat_path, + sample_rate=2000, + n_channels_dat=dataset.nc, + ) # create some sparse PC features n_subch = int(np.round(mtemp.n_channels / 2) - 1) @@ -123,7 +133,11 @@ def test_spike_depths(dataset): np.save(path / 'channel_positions.npy', mtemp.channel_positions) model = TemplateModel( - dir_path=path, dat_path=dataset.dat_path, sample_rate=2000, n_channels_dat=dataset.nc) + dir_path=path, + dat_path=dataset.dat_path, + sample_rate=2000, + n_channels_dat=dataset.nc, + ) c = EphysAlfCreator(model) shutil.rmtree(out_path, ignore_errors=True) @@ -154,7 +168,11 @@ def test_creator(dataset): out_path = path / 'alf' model = TemplateModel( - dir_path=path, dat_path=dataset.dat_path, sample_rate=2000, n_channels_dat=dataset.nc) + dir_path=path, + dat_path=dataset.dat_path, + sample_rate=2000, + n_channels_dat=dataset.nc, + ) c = EphysAlfCreator(model) with raises(IOError): @@ -189,11 +207,15 @@ def check_conversion_output(): assert len(set(ch_shape)) == 1 dur = np.load(next(out_path.glob('clusters.peakToTrough*.npy'))) - assert np.all(dur == np.array([-9.5, 3., 13., -4.5, -2.5])) + assert np.all(dur == np.array([-9.5, 3.0, 13.0, -4.5, -2.5])) def read_after_write(): - model = TemplateModel(dir_path=out_path, dat_path=dataset.dat_path, - sample_rate=2000, n_channels_dat=dataset.nc) + model = TemplateModel( + dir_path=out_path, + dat_path=dataset.dat_path, + sample_rate=2000, + n_channels_dat=dataset.nc, + ) np.all(model.spike_templates == c.model.spike_templates) np.all(model.spike_times == c.model.spike_times) @@ -213,12 +235,15 @@ def read_after_write(): def test_merger(dataset): - path = Path(dataset.tmp_dir) out_path = path / 'alf' model = TemplateModel( - dir_path=path, dat_path=dataset.dat_path, sample_rate=2000, n_channels_dat=dataset.nc) + dir_path=path, + dat_path=dataset.dat_path, + sample_rate=2000, + n_channels_dat=dataset.nc, + ) c = EphysAlfCreator(model) c.convert(out_path) @@ -235,18 +260,23 @@ def test_merger(dataset): # merge the first two clusters merge_clu = clu[0:2] - spike_clusters[np.bitwise_or(spike_clusters == clu[0], - spike_clusters == clu[1])] = np.max(clu) + 1 + spike_clusters[np.bitwise_or(spike_clusters == clu[0], spike_clusters == clu[1])] = ( + np.max(clu) + 1 + ) # split the cluster with the most spikes split_clu = clu[-1] idx = np.where(spike_clusters == split_clu)[0] - spike_clusters[idx[0:int(n_clu[-1] / 2)]] = np.max(clu) + 2 - spike_clusters[idx[int(n_clu[-1] / 2):]] = np.max(clu) + 3 + spike_clusters[idx[0 : int(n_clu[-1] / 2)]] = np.max(clu) + 2 + spike_clusters[idx[int(n_clu[-1] / 2) :]] = np.max(clu) + 3 np.save(path / 'spike_clusters.npy', spike_clusters) model = TemplateModel( - dir_path=path, dat_path=dataset.dat_path, sample_rate=2000, n_channels_dat=dataset.nc) + dir_path=path, + dat_path=dataset.dat_path, + sample_rate=2000, + n_channels_dat=dataset.nc, + ) c = EphysAlfCreator(model) c.convert(out_path_merge) diff --git a/phylib/io/tests/test_array.py b/phylib/io/tests/test_array.py index 594e9fd..8aba109 100644 --- a/phylib/io/tests/test_array.py +++ b/phylib/io/tests/test_array.py @@ -1,30 +1,46 @@ -# -*- coding: utf-8 -*- - """Tests of array utility functions.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ from pathlib import Path import numpy as np from pytest import raises -from ..array import ( - _unique, _normalize, _index_of, _spikes_in_clusters, _spikes_per_cluster, - _flatten_per_cluster, get_closest_clusters, _get_data_lim, _flatten, _clip, - chunk_bounds, excerpts, data_chunk, grouped_mean, SpikeSelector, - get_excerpts, _range_from_slice, _pad, _get_padded, - read_array, write_array) from phylib.utils._types import _as_array from phylib.utils.testing import _assert_equal as ae -from ..mock import artificial_spike_clusters, artificial_spike_samples +from ..array import ( + SpikeSelector, + _clip, + _flatten, + _flatten_per_cluster, + _get_data_lim, + _get_padded, + _index_of, + _normalize, + _pad, + _range_from_slice, + _spikes_in_clusters, + _spikes_per_cluster, + _unique, + chunk_bounds, + data_chunk, + excerpts, + get_closest_clusters, + get_excerpts, + grouped_mean, + read_array, + write_array, +) +from ..mock import artificial_spike_clusters, artificial_spike_samples -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test utility functions -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_clip(): assert _clip(-1, 0, 1) == 0 @@ -33,8 +49,9 @@ def test_clip(): def test_range_from_slice(): """Test '_range_from_slice'.""" - class _SliceTest(object): + class _SliceTest: """Utility class to make it more convenient to test slice objects.""" + def __init__(self, **kwargs): self._kwargs = kwargs @@ -144,10 +161,10 @@ def test_normalize(): x_min, y_min = positions_n.min(axis=0) x_max, y_max = positions_n.max(axis=0) - np.allclose(x_min, 0.) - np.allclose(x_max, 1.) - np.allclose(y_min, 0.) - np.allclose(y_max, 1.) + np.allclose(x_min, 0.0) + np.allclose(x_max, 1.0) + np.allclose(y_min, 0.0) + np.allclose(y_max, 1.0) # Keep ratio is True. positions_n = _normalize(positions, keep_ratio=True) @@ -155,8 +172,8 @@ def test_normalize(): x_min, y_min = positions_n.min(axis=0) x_max, y_max = positions_n.max(axis=0) - np.allclose(min(x_min, y_min), 0.) - np.allclose(max(x_max, y_max), 1.) + np.allclose(min(x_min, y_min), 0.0) + np.allclose(max(x_max, y_max), 1.0) np.allclose(x_min + x_max, 1) np.allclose(y_min + y_max, 1) @@ -171,8 +188,8 @@ def test_index_of(): def test_as_array(): ae(_as_array(3), [3]) ae(_as_array([3]), [3]) - ae(_as_array(3.), [3.]) - ae(_as_array([3.]), [3.]) + ae(_as_array(3.0), [3.0]) + ae(_as_array([3.0]), [3.0]) with raises(ValueError): _as_array(map) @@ -187,9 +204,10 @@ def test_get_closest_clusters(): assert [_ for _, __ in out] == [2, 1, 0] -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test read/save -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_read_write(tempdir): arr = np.arange(10).astype(np.float32) @@ -201,9 +219,10 @@ def test_read_write(tempdir): ae(read_array(path, mmap_mode='r'), arr) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test chunking -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_chunk_bounds(): chunks = chunk_bounds(200, 100, overlap=20) @@ -240,16 +259,12 @@ def test_chunk(): def test_excerpts_1(): - bounds = [(start, end) for (start, end) in excerpts(100, - n_excerpts=3, - excerpt_size=10)] + bounds = [(start, end) for (start, end) in excerpts(100, n_excerpts=3, excerpt_size=10)] assert bounds == [(0, 10), (45, 55), (90, 100)] def test_excerpts_2(): - bounds = [(start, end) for (start, end) in excerpts(10, - n_excerpts=3, - excerpt_size=10)] + bounds = [(start, end) for (start, end) in excerpts(10, n_excerpts=3, excerpt_size=10)] assert bounds == [(0, 10)] @@ -271,9 +286,10 @@ def test_get_excerpts(): assert len(get_excerpts(data, n_excerpts=0, excerpt_size=10)) == 0 -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test spike clusters functions -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_spikes_in_clusters(): """Test _spikes_in_clusters().""" @@ -288,8 +304,7 @@ def test_spikes_in_clusters(): assert np.all(spike_clusters[_spikes_in_clusters(spike_clusters, [i])] == i) clusters = [1, 2, 3] - assert np.all(np.isin( - spike_clusters[_spikes_in_clusters(spike_clusters, clusters)], clusters)) + assert np.all(np.isin(spike_clusters[_spikes_in_clusters(spike_clusters, clusters)], clusters)) def test_spikes_per_cluster(): @@ -321,12 +336,13 @@ def test_grouped_mean(): ae(grouped_mean(arr, spike_clusters), [10, -3, -5]) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test spike selection -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_select_spikes_1(): - spike_times = np.array([0., 1., 2., 3.3, 4.4]) + spike_times = np.array([0.0, 1.0, 2.0, 3.3, 4.4]) spike_clusters = np.array([1, 2, 1, 2, 4]) chunk_bounds = [0.0, 1.1, 2.2, 3.3, 4.4, 5.5, 6.6] n_chunks_kept = 2 @@ -336,7 +352,10 @@ def test_select_spikes_1(): spc = _spikes_per_cluster(spike_clusters) ss = SpikeSelector( get_spikes_per_cluster=lambda cl: spc.get(cl, np.array([], dtype=np.int64)), - spike_times=spike_times, chunk_bounds=chunk_bounds, n_chunks_kept=n_chunks_kept) + spike_times=spike_times, + chunk_bounds=chunk_bounds, + n_chunks_kept=n_chunks_kept, + ) ae(ss.chunks_kept, [0.0, 1.1, 3.3, 4.4]) ae(ss(3, [], subset_chunks=True), []) @@ -357,16 +376,19 @@ def test_select_spikes_2(): n_spikes = 1000 n_clusters = 10 spike_times = artificial_spike_samples(n_spikes) - spike_times = 10. * spike_times / spike_times.max() + spike_times = 10.0 * spike_times / spike_times.max() chunk_bounds = np.linspace(0.0, 10.0, 11) n_chunks_kept = 3 - chunks_kept = [0., 1., 4., 5., 8., 9.] + chunks_kept = [0.0, 1.0, 4.0, 5.0, 8.0, 9.0] spike_clusters = artificial_spike_clusters(n_spikes, n_clusters) spc = _spikes_per_cluster(spike_clusters) ss = SpikeSelector( get_spikes_per_cluster=lambda cl: spc.get(cl, np.array([], dtype=np.int64)), - spike_times=spike_times, chunk_bounds=chunk_bounds, n_chunks_kept=n_chunks_kept) + spike_times=spike_times, + chunk_bounds=chunk_bounds, + n_chunks_kept=n_chunks_kept, + ) ae(ss.chunks_kept, chunks_kept) def _check_chunks(sid): diff --git a/phylib/io/tests/test_datasets.py b/phylib/io/tests/test_datasets.py index 6651593..9d95b92 100644 --- a/phylib/io/tests/test_datasets.py +++ b/phylib/io/tests/test_datasets.py @@ -1,53 +1,55 @@ -# -*- coding: utf-8 -*- - """Tests of dataset utility functions.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import logging -from pathlib import Path from itertools import product +from pathlib import Path import numpy as np -from numpy.testing import assert_array_equal as ae import responses -from pytest import raises, fixture +from numpy.testing import assert_array_equal as ae +from pytest import fixture, raises +from requests.exceptions import HTTPError, RequestException -from ..datasets import (download_file, - download_test_file, - _check_md5_of_url, - ) from phylib.utils.testing import captured_logging +from ..datasets import ( + _check_md5_of_url, + download_file, + download_test_file, +) + logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Fixtures -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test URL and data _URL = 'http://test/data' -_DATA = np.linspace(0., 1., 100000).astype(np.float32) +_DATA = np.linspace(0.0, 1.0, 100000).astype(np.float32) _CHECKSUM = '7d257d0ae7e3af8ca3574ccc3a4bf072' def _add_mock_response(url, body, file_type='binary'): - content_type = ('application/octet-stream' - if file_type == 'binary' else 'text/plain') - responses.add(responses.GET, url, - body=body, - status=200, - content_type=content_type, - ) + content_type = 'application/octet-stream' if file_type == 'binary' else 'text/plain' + responses.add( + responses.GET, + url, + body=body, + status=200, + content_type=content_type, + ) @fixture def mock_url(): _add_mock_response(_URL, _DATA.tobytes()) - _add_mock_response(_URL + '.md5', _CHECKSUM + ' ' + Path(_URL).name) + _add_mock_response(f'{_URL}.md5', f'{_CHECKSUM} {Path(_URL).name}') yield _URL responses.reset() @@ -57,7 +59,7 @@ def mock_urls(request): data = _DATA.tobytes() checksum = _CHECKSUM url_data = _URL - url_checksum = _URL + '.md5' + url_checksum = f'{_URL}.md5' if not request.param[0]: # Data URL is corrupted. @@ -90,9 +92,10 @@ def _check(data): ae(np.frombuffer(data, np.float32), _DATA) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test utility functions -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + @responses.activate def test_check_md5_of_url(tempdir, mock_url): @@ -101,15 +104,16 @@ def test_check_md5_of_url(tempdir, mock_url): assert _check_md5_of_url(output_path, _URL) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test download functions -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + @responses.activate def test_download_not_found(tempdir): path = Path(tempdir) / 'test' - with raises(Exception): - download_file(_URL + '_notfound', path) + with raises(RequestException): + download_file(f'{_URL}_notfound', path) @responses.activate @@ -139,17 +143,20 @@ def test_download_file(tempdir, mock_urls): param, url_data, url_checksum = mock_urls data_here, data_valid, checksum_here, checksum_valid = param - assert_succeeds = (data_here and data_valid and - ((checksum_here == checksum_valid) or - (not(checksum_here) and checksum_valid))) + assert_succeeds = ( + data_here + and data_valid + and ((checksum_here == checksum_valid) or (not (checksum_here) and checksum_valid)) + ) - download_succeeds = (assert_succeeds or (data_here and - (not(data_valid) and not(checksum_here)))) + download_succeeds = assert_succeeds or ( + data_here and (not (data_valid) and not (checksum_here)) + ) if download_succeeds: data = _dl(path) else: - with raises(Exception): + with raises(RequestException): data = _dl(path) if assert_succeeds: diff --git a/phylib/io/tests/test_merge.py b/phylib/io/tests/test_merge.py index 0907ba6..ad472b8 100644 --- a/phylib/io/tests/test_merge.py +++ b/phylib/io/tests/test_merge.py @@ -1,23 +1,22 @@ -# -*- coding: utf-8 -*- - """Test probe merging.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import numpy as np -from ..merge import Merger from phylib.io.alf import EphysAlfCreator from phylib.io.model import load_model from phylib.io.tests.conftest import _make_dataset +from ..merge import Merger -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Merging tests -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_probe_merge_1(tempdir): out_dir = tempdir / 'merged' @@ -78,8 +77,9 @@ def test_merged_single(merged, merged_original_amps=None): if merged_original_amps is None: merged_original_amps = merged.amplitudes _, im1, i1 = np.intersect1d(merged_original_amps, single.amplitudes, return_indices=True) - _, im2, i2 = np.intersect1d(merged_original_amps, single.amplitudes + 20, - return_indices=True) + _, im2, i2 = np.intersect1d( + merged_original_amps, single.amplitudes + 20, return_indices=True + ) # intersection spans the full vector assert i1.size + i2.size == merged.amplitudes.size # test spikes @@ -92,8 +92,9 @@ def test_merged_single(merged, merged_original_amps=None): assert np.all(merged.spike_templates[im1] - single.spike_templates[i1] == 0) assert np.all(merged.spike_templates[im2] - single.spike_templates[i2] == 64) # test probes - assert np.all(merged.channel_probes == np.r_[single.channel_probes, - single.channel_probes + 1]) + assert np.all( + merged.channel_probes == np.r_[single.channel_probes, single.channel_probes + 1] + ) assert np.all(merged.templates_channels[merged.templates_probes == 0] < single.n_channels) assert np.all(merged.templates_channels[merged.templates_probes == 1] >= single.n_channels) spike_probes = merged.templates_probes[merged.spike_templates] @@ -114,8 +115,11 @@ def test_merged_single(merged, merged_original_amps=None): assert np.all(chid == np.r_[single.channel_mapping, single.channel_mapping]) out_files = list(tempdir.joinpath('alf').glob('*.*')) - cl_shape = [np.load(f).shape[0] for f in out_files if f.name.startswith('clusters.') and - f.name.endswith('.npy')] + cl_shape = [ + np.load(f).shape[0] + for f in out_files + if f.name.startswith('clusters.') and f.name.endswith('.npy') + ] sp_shape = [np.load(f).shape[0] for f in out_files if f.name.startswith('spikes.')] ch_shape = [np.load(f).shape[0] for f in out_files if f.name.startswith('channels.')] assert len(set(cl_shape)) == 1 diff --git a/phylib/io/tests/test_mock.py b/phylib/io/tests/test_mock.py index e53e180..eff788b 100644 --- a/phylib/io/tests/test_mock.py +++ b/phylib/io/tests/test_mock.py @@ -1,27 +1,26 @@ -# -*- coding: utf-8 -*- - """Tests of mock datasets.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import numpy as np from numpy.testing import assert_array_equal as ae -from ..mock import (artificial_waveforms, - artificial_traces, - artificial_spike_clusters, - artificial_features, - artificial_masks, - artificial_spike_samples, - artificial_correlograms, - ) - - -#------------------------------------------------------------------------------ +from ..mock import ( + artificial_correlograms, + artificial_features, + artificial_masks, + artificial_spike_clusters, + artificial_spike_samples, + artificial_traces, + artificial_waveforms, +) + +# ------------------------------------------------------------------------------ # Tests -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _test_artificial(n_spikes=None, n_clusters=None): n_samples_waveforms = 32 @@ -30,19 +29,17 @@ def _test_artificial(n_spikes=None, n_clusters=None): n_features = n_channels * 2 # Waveforms. - waveforms = artificial_waveforms(n_spikes=n_spikes, - n_samples=n_samples_waveforms, - n_channels=n_channels) + waveforms = artificial_waveforms( + n_spikes=n_spikes, n_samples=n_samples_waveforms, n_channels=n_channels + ) assert waveforms.shape == (n_spikes, n_samples_waveforms, n_channels) # Traces. - traces = artificial_traces(n_samples=n_samples_traces, - n_channels=n_channels) + traces = artificial_traces(n_samples=n_samples_traces, n_channels=n_channels) assert traces.shape == (n_samples_traces, n_channels) # Spike clusters. - spike_clusters = artificial_spike_clusters(n_spikes=n_spikes, - n_clusters=n_clusters) + spike_clusters = artificial_spike_clusters(n_spikes=n_spikes, n_clusters=n_clusters) assert spike_clusters.shape == (n_spikes,) if n_clusters >= 1: assert spike_clusters.min() in (0, 1) diff --git a/phylib/io/tests/test_model.py b/phylib/io/tests/test_model.py index f9b32be..012521d 100644 --- a/phylib/io/tests/test_model.py +++ b/phylib/io/tests/test_model.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- - """Testing the Template model.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import logging @@ -14,14 +12,16 @@ # from phylib.utils import Bunch from phylib.utils.testing import captured_output + from ..model import from_sparse, load_model logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Tests -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_from_sparse(): data = np.array([[0, 1, 2], [3, 4, 5]]) @@ -60,7 +60,11 @@ def test_model_2(template_model_full): spike_ids = m.get_cluster_spikes(3) w = m.get_waveforms(spike_ids, channel_ids) - assert w is None or w.shape == (len(spike_ids), tmp.template.shape[0], len(channel_ids)) + assert w is None or w.shape == ( + len(spike_ids), + tmp.template.shape[0], + len(channel_ids), + ) f = m.get_features(spike_ids, channel_ids) assert f is None or f.shape == (len(spike_ids), len(channel_ids), 3) @@ -170,7 +174,7 @@ def test_model_spike_waveforms(template_path_full): # Check each array with the ground truth, obtained from the raw data. for i, spike in enumerate(spike_ids): t = int(model.spike_samples[spike]) - wt = traces[t - nsw:t + nsw, channel_ids] + wt = traces[t - nsw : t + nsw, channel_ids] ae(waveforms[tid][i], wt) ae(w[i], wt) @@ -215,6 +219,6 @@ def test_model_metadata_3(template_model): def test_model_spike_attributes(template_model_full): model = template_model_full - assert set(model.spike_attributes.keys()) == set(('randn', 'works')) + assert set(model.spike_attributes.keys()) == {'randn', 'works'} assert model.spike_attributes.works.shape == (model.n_spikes,) assert model.spike_attributes.randn.shape == (model.n_spikes, 2) diff --git a/phylib/io/tests/test_traces.py b/phylib/io/tests/test_traces.py index 18f2663..4f47f76 100644 --- a/phylib/io/tests/test_traces.py +++ b/phylib/io/tests/test_traces.py @@ -1,31 +1,37 @@ -# -*- coding: utf-8 -*- - """Testing the BaseEphysTraces class.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import logging +import mtscomp import numpy as np -from numpy.testing import assert_equal as ae from numpy.testing import assert_allclose as ac -import mtscomp -from pytest import raises, fixture, mark +from numpy.testing import assert_equal as ae +from pytest import fixture, mark, raises from phylib.utils import Bunch + from ..traces import ( - _get_subitems, _get_chunk_bounds, - get_ephys_reader, BaseEphysReader, extract_waveforms, export_waveforms, RandomEphysReader, - get_spike_waveforms) + BaseEphysReader, + RandomEphysReader, + _get_chunk_bounds, + _get_subitems, + export_waveforms, + extract_waveforms, + get_ephys_reader, + get_spike_waveforms, +) logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test utils -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_get_subitems(): bounds = [0, 2, 5] @@ -88,9 +94,10 @@ def _a(x, y, z): _a([3, 7, 5], 10, [0, 3, 10, 15]) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test ephys reader -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + @fixture def arr(): @@ -117,17 +124,22 @@ def traces(request, tempdir, arr, sample_rate): with open(path, 'wb') as f: arr.tofile(f) return get_ephys_reader( - path, sample_rate=sample_rate, dtype=arr.dtype, n_channels=arr.shape[1]) + path, sample_rate=sample_rate, dtype=arr.dtype, n_channels=arr.shape[1] + ) elif request.param == 'flat_concat': path0 = tempdir / 'data0.bin' with open(path0, 'wb') as f: - arr[:arr.shape[0] // 2, :].tofile(f) + arr[: arr.shape[0] // 2, :].tofile(f) path1 = tempdir / 'data1.bin' with open(path1, 'wb') as f: - arr[arr.shape[0] // 2:, :].tofile(f) + arr[arr.shape[0] // 2 :, :].tofile(f) return get_ephys_reader( - [path0, path1], sample_rate=sample_rate, dtype=arr.dtype, n_channels=arr.shape[1]) + [path0, path1], + sample_rate=sample_rate, + dtype=arr.dtype, + n_channels=arr.shape[1], + ) elif request.param in ('mtscomp', 'mtscomp_reader'): path = tempdir / 'data.bin' @@ -136,9 +148,16 @@ def traces(request, tempdir, arr, sample_rate): out = tempdir / 'data.cbin' outmeta = tempdir / 'data.ch' mtscomp.compress( - path, out, outmeta, sample_rate=sample_rate, - n_channels=arr.shape[1], dtype=arr.dtype, - n_threads=1, check_after_compress=False, quiet=True) + path, + out, + outmeta, + sample_rate=sample_rate, + n_channels=arr.shape[1], + dtype=arr.dtype, + n_threads=1, + check_after_compress=False, + quiet=True, + ) reader = mtscomp.decompress(out, outmeta, check_after_decompress=False, quiet=True) if request.param == 'mtscomp': return get_ephys_reader(reader) @@ -174,14 +193,14 @@ def _a(f): _a(lambda x: x * 2) _a(lambda x: 2 * x) - _a(lambda x: x ** 2) - _a(lambda x: 2 ** x) + _a(lambda x: x**2) + _a(lambda x: 2**x) _a(lambda x: x / 2) _a(lambda x: 2 / x) - _a(lambda x: x / 2.) - _a(lambda x: 2. / x) + _a(lambda x: x / 2.0) + _a(lambda x: 2.0 / x) _a(lambda x: x // 2) _a(lambda x: 2 // x) @@ -247,8 +266,13 @@ def test_waveform_extractor(tempdir, arr, traces, sample_rate, do_export, do_cac # Export waveforms into a npy file. if do_export: export_waveforms( - tempdir / 'waveforms.npy', traces, spike_samples, spike_channels, - n_samples_waveforms=nsw, cache=do_cache) + tempdir / 'waveforms.npy', + traces, + spike_samples, + spike_channels, + n_samples_waveforms=nsw, + cache=do_cache, + ) w = np.load(tempdir / 'waveforms.npy') # Extract waveforms directly. else: @@ -263,8 +287,8 @@ def test_waveform_extractor(tempdir, arr, traces, sample_rate, do_export, do_cac ) ww = get_spike_waveforms( - spike_ids, channel_ids, spike_waveforms=spike_waveforms, - n_samples_waveforms=nsw) + spike_ids, channel_ids, spike_waveforms=spike_waveforms, n_samples_waveforms=nsw + ) ae(w, ww) assert np.all(w[0, :5, :] == 0) diff --git a/phylib/io/traces.py b/phylib/io/traces.py index e9f423b..9a6a867 100644 --- a/phylib/io/traces.py +++ b/phylib/io/traces.py @@ -1,24 +1,28 @@ -# -*- coding: utf-8 -*- - """EphysReader class.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import copy import logging +import multiprocessing as mp from functools import reduce from math import ceil -import multiprocessing as mp from operator import mul from pathlib import Path -import numpy as np -from numpy.lib.format import ( - _check_version, _write_array_header, dtype_to_descr) import mtscomp +import numpy as np + +try: + # NumPy 2.0+ + from numpy.lib.format import dtype_to_descr + from numpy.lib.format import write_array_header_1_0 as _write_array_header +except ImportError: + # NumPy 1.x fallback + from numpy.lib.format import _write_array_header, dtype_to_descr from tqdm import tqdm from .array import _index_of @@ -33,9 +37,17 @@ EPHYS_RAW_EXTENSIONS = ('.dat', '.bin', '.raw', '.mda') -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Utils -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + + +# NumPy 2.0 compatibility: _check_version was removed +def _check_version(version): + """Check numpy format version. Compatible with NumPy 1.x and 2.x.""" + if version is not None and version != (1, 0): + raise ValueError(f'Unsupported format version: {version}') + def prod(l): return reduce(mul, l, 1) @@ -66,7 +78,7 @@ def _get_subitems(bounds, item): first_chunk, last_chunk = _find_chunks(bounds, [start, stop - 1]) out = [] for chunk in range(first_chunk, last_chunk + 1): - i0, i1 = bounds[chunk:chunk + 2] + i0, i1 = bounds[chunk : chunk + 2] chunk_start = max(0, start - i0) chunk_stop = min(i1 - i0, stop - i0) assert chunk_start >= 0 @@ -84,7 +96,7 @@ def _get_subitems(bounds, item): for chunk in np.unique(chunks): if chunk >= len(bounds) - 1: raise IndexError() - i0, i1 = bounds[chunk:chunk + 2] + i0, i1 = bounds[chunk : chunk + 2] out.append((chunk, item[(i0 <= item) & (item < i1)] - i0)) return out elif isinstance(item, tuple): @@ -148,7 +160,7 @@ def _get_chunk_bounds(arr_sizes, chunk_size): def _apply_op(op, arg, arr): if op == 'cols': return arr[:, arg] - f = getattr(arr, '__%s__' % op) + f = getattr(arr, f'__{op}__') return f(arg) if arg is not None else f() @@ -159,18 +171,20 @@ def _memmap_flat(path, dtype=None, n_channels=None, offset=0, mode='r+'): fsize = path.stat().st_size item_size = np.dtype(dtype).itemsize if (fsize - offset) % (item_size * n_channels) != 0: - logger.warning('Inconsistent number of channels between the ' - 'params file and the binary dat file') + logger.warning( + 'Inconsistent number of channels between the params file and the binary dat file' + ) n_samples = (fsize - offset) // (item_size * n_channels) shape = (n_samples, n_channels) return np.memmap(path, dtype=dtype, offset=offset, shape=shape, mode=mode) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # EphysReader -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + -class BaseEphysReader(object): +class BaseEphysReader: # To be set in child classes: sample_rate = 0 n_channels = 0 @@ -306,9 +320,17 @@ def iter_chunks(self, cache=True): class FlatEphysReader(BaseEphysReader): - def __init__(self, paths, sample_rate=None, dtype=None, offset=0, n_channels=None, mode='r', - **kwargs): - super(FlatEphysReader, self).__init__() + def __init__( + self, + paths, + sample_rate=None, + dtype=None, + offset=0, + n_channels=None, + mode='r', + **kwargs, + ): + super().__init__() if isinstance(paths, (str, Path)): paths = [paths] self._paths = [Path(p) for p in paths] @@ -317,7 +339,8 @@ def __init__(self, paths, sample_rate=None, dtype=None, offset=0, n_channels=Non self.dir_path = paths[0].parent self._mmaps = [ _memmap_flat(path, dtype=dtype, n_channels=n_channels, offset=offset, mode=mode) - for path in paths] + for path in paths + ] self.sample_rate = sample_rate self.dtype = dtype @@ -325,7 +348,8 @@ def __init__(self, paths, sample_rate=None, dtype=None, offset=0, n_channels=Non chunk_size = int(round(DEFAULT_CHUNK_DURATION * sample_rate)) self.part_bounds = _get_part_bounds(self._mmaps) self.chunk_bounds = _get_chunk_bounds( - [arr.shape[0] for arr in self._mmaps], chunk_size=chunk_size) + [arr.shape[0] for arr in self._mmaps], chunk_size=chunk_size + ) assert self.sample_rate > 0 assert self.n_channels >= 0 @@ -337,14 +361,15 @@ def _get_part(self, part_idx, subitem): class MtscompEphysReader(BaseEphysReader): def __init__(self, reader, **kwargs): - super(MtscompEphysReader, self).__init__() + super().__init__() if isinstance(reader, (tuple, list)): # pragma: no cover assert reader # TODO: support concatenation of multiple .cbin files. Currently, only take the first. if len(reader) >= 2: logger.warning( - "Support of multiple concatenate .cbin files is not supported yet. " - "Taking the first file only.") + 'Support of multiple concatenate .cbin files is not supported yet. ' + 'Taking the first file only.' + ) reader = reader[0] assert isinstance(reader, mtscomp.Reader) self.reader = reader @@ -353,7 +378,10 @@ def __init__(self, reader, **kwargs): self.sample_rate = reader.sample_rate self.dtype = reader.dtype self.n_channels = reader.n_channels - self.part_bounds = [0, reader.n_samples] # TODO: support multiple concatenated readers + self.part_bounds = [ + 0, + reader.n_samples, + ] # TODO: support multiple concatenated readers self.chunk_bounds = reader.chunk_bounds def _get_part(self, part_idx, subitem): @@ -377,9 +405,11 @@ def iter_chunks(self, cache=True): if cache: logger.debug( - "Processing batch #%d/%d with chunks %s.", + 'Processing batch #%d/%d with chunks %s.', batch + 1, - reader.n_batches, ', '.join(map(str, range(first_chunk, last_chunk)))) + reader.n_batches, + ', '.join(map(str, range(first_chunk, last_chunk))), + ) # Decompress all chunks in the batch. reader.decompress_chunks(range(first_chunk, last_chunk), reader.pool) @@ -398,7 +428,7 @@ def iter_chunks(self, cache=True): class ArrayEphysReader(BaseEphysReader): def __init__(self, arr, **kwargs): - super(ArrayEphysReader, self).__init__() + super().__init__() self._arr = arr self.sample_rate = kwargs.pop('sample_rate', None) assert self.sample_rate > 0 @@ -417,20 +447,20 @@ class NpyEphysReader(ArrayEphysReader): def __init__(self, path, **kwargs): if isinstance(path, (tuple, list)): if len(path) != 1: - raise ValueError("There should be exactly one path to a npy file.") + raise ValueError('There should be exactly one path to a npy file.') path = path[0] path = Path(path) self.name = path.stem self.dir_path = path.parent self._arr = np.load(path, mmap_mode='r') # TODO: support for multiple npy files - super(NpyEphysReader, self).__init__(self._arr, **kwargs) + super().__init__(self._arr, **kwargs) class RandomEphysReader(BaseEphysReader): name = 'random' def __init__(self, n_samples, n_channels, sample_rate=None, **kwargs): - super(RandomEphysReader, self).__init__() + super().__init__() self.sample_rate = sample_rate assert self.sample_rate > 0 self.dtype = np.float32 @@ -445,9 +475,10 @@ def _get_part(self, part_idx, subitem): return np.random.randn(n, self.n_channels).astype(np.float32) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # High-level functions -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _get_ephys_constructor(obj, **kwargs): """Return the class, argument, and kwargs to create an Ephys instance from any @@ -459,11 +490,11 @@ def _get_ephys_constructor(obj, **kwargs): elif isinstance(obj, (str, Path)): path = Path(obj) if not path.exists(): # pragma: no cover - logger.warning("File %s does not exist.", path) + logger.warning('File %s does not exist.', path) return None, None, {} assert path.exists() ext = path.suffix - assert ext, "No extension found in file `%s`" % path + assert ext, f'No extension found in file `{path}`' # Mtscomp file if ext == '.cbin': reader = mtscomp.Reader(n_threads=mp.cpu_count() // 2) @@ -476,7 +507,7 @@ def _get_ephys_constructor(obj, **kwargs): return (NpyEphysReader, obj, kwargs) # TODO: other standard binary formats else: # pragma: no cover - raise IOError("Unknown file extension: `%s`." % ext) + raise OSError(f'Unknown file extension: `{ext}`.') elif isinstance(obj, (tuple, list)): if obj: # Concatenate the main argument to the constructor. @@ -499,9 +530,10 @@ def get_ephys_reader(obj, **kwargs): return klass(arg, **kwargs) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Waveform extractor -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def get_spike_waveforms(spike_ids, channel_ids, spike_waveforms=None, n_samples_waveforms=None): """Get spike waveforms from precomputed doubly sparse spike waveforms array. @@ -547,7 +579,7 @@ def _npy_header(shape, dtype, order='C'): # pragma: no cover return d -class NpyWriter(object): +class NpyWriter: def __init__(self, path, shape, dtype, axis=0): assert axis == 0 # only concatenation along the first axis is supported right now # Only C order is supported at the moment. @@ -557,7 +589,14 @@ def __init__(self, path, shape, dtype, axis=0): version = None _check_version(version) self.fp = open(path, 'wb') - _write_array_header(self.fp, header, version) + + # NumPy 2.0 compatibility: write_array_header_1_0 has different signature + try: + # NumPy 2.0+ signature: write_array_header_1_0(fp, header) + _write_array_header(self.fp, header) + except TypeError: + # NumPy 1.x signature: _write_array_header(fp, header, version) + _write_array_header(self.fp, header, version) def append(self, chunk): if chunk.ndim == len(self.shape): @@ -586,7 +625,7 @@ def _extract_waveform(traces, sample, channel_ids=None, n_samples_waveforms=None n_channels = len(channel_ids) t0, t1 = int(sample - a), int(sample + b) # Extract the waveforms. - w = traces[max(0, t0):t1][:, channel_ids] + w = traces[max(0, t0) : t1][:, channel_ids] if not isinstance(channel_ids, slice): w[:, channel_ids == -1] = 0 # Deal with side effects. @@ -603,13 +642,14 @@ def extract_waveforms(traces, spike_samples, channel_ids, n_samples_waveforms=No # Create the output array. ns = len(spike_samples) nsw = n_samples_waveforms - assert nsw > 0, "Please specify n_samples_waveforms > 0" + assert nsw > 0, 'Please specify n_samples_waveforms > 0' nc = len(channel_ids) # Extract the spike waveforms. out = np.zeros((ns, nsw, nc), dtype=traces.dtype) for i, ts in enumerate(spike_samples): - out[i] = _extract_waveform( - traces, ts, channel_ids=channel_ids, n_samples_waveforms=nsw)[np.newaxis, ...] + out[i] = _extract_waveform(traces, ts, channel_ids=channel_ids, n_samples_waveforms=nsw)[ + np.newaxis, ... + ] return out @@ -624,7 +664,7 @@ def iter_waveforms(traces, spike_samples, spike_channels, n_samples_waveforms=No n_samples_waveforms = n_samples_waveforms n_channels_loc = spike_channels.shape[1] - pb = tqdm(desc="Extracting waveforms", total=traces.duration) + pb = tqdm(desc='Extracting waveforms', total=traces.duration) for i0, i1 in traces.iter_chunks(cache=cache): # Get spikes in chunk. ind = _find_chunks([i0, i1], spike_samples) == 0 @@ -639,15 +679,24 @@ def iter_waveforms(traces, spike_samples, spike_channels, n_samples_waveforms=No for i, s in enumerate(ss): channel_ids = sc[i, :] waveforms[i, ...] = _extract_waveform( - traces, int(s), channel_ids=channel_ids, - n_samples_waveforms=n_samples_waveforms) + traces, + int(s), + channel_ids=channel_ids, + n_samples_waveforms=n_samples_waveforms, + ) yield waveforms pb.close() def export_waveforms( - path, traces, spike_samples, spike_channels, n_samples_waveforms=None, cache=False, - sample2unit=1): + path, + traces, + spike_samples, + spike_channels, + n_samples_waveforms=None, + cache=False, + sample2unit=1, +): """Export a selection of spike waveforms to a npy file by iterating over the data on a chunk by chunk basis.""" n_spikes = len(spike_samples) @@ -658,8 +707,12 @@ def export_waveforms( writer = NpyWriter(path, shape, dtype) size_written = 0 for waveforms in iter_waveforms( - traces, spike_samples, spike_channels, n_samples_waveforms=n_samples_waveforms, - cache=cache): + traces, + spike_samples, + spike_channels, + n_samples_waveforms=n_samples_waveforms, + cache=cache, + ): writer.append(waveforms * sample2unit) size_written += waveforms.size writer.close() diff --git a/phylib/stats/ccg.py b/phylib/stats/ccg.py index 08febc7..7cba2a5 100644 --- a/phylib/stats/ccg.py +++ b/phylib/stats/ccg.py @@ -1,20 +1,18 @@ -# -*- coding: utf-8 -*- - """Cross-correlograms.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import numpy as np -from phylib.utils._types import _as_array from phylib.io.array import _index_of, _unique +from phylib.utils._types import _as_array - -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Cross-correlograms -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _increment(arr, indices): """Increment some indices in a 1D vector of non-negative integers. @@ -22,18 +20,17 @@ def _increment(arr, indices): arr = _as_array(arr) indices = _as_array(indices) bbins = np.bincount(indices) - arr[:len(bbins)] += bbins + arr[: len(bbins)] += bbins return arr def _diff_shifted(arr, steps=1): arr = _as_array(arr) - return arr[steps:] - arr[:len(arr) - steps] + return arr[steps:] - arr[: len(arr) - steps] def _create_correlograms_array(n_clusters, winsize_bins): - return np.zeros((n_clusters, n_clusters, winsize_bins // 2 + 1), - dtype=np.int32) + return np.zeros((n_clusters, n_clusters, winsize_bins // 2 + 1), dtype=np.int32) def _symmetrize_correlograms(correlograms): @@ -45,8 +42,7 @@ def _symmetrize_correlograms(correlograms): # We symmetrize c[i, j, 0]. # This is necessary because the algorithm in correlograms() # is sensitive to the order of identical spikes. - correlograms[..., 0] = np.maximum(correlograms[..., 0], - correlograms[..., 0].T) + correlograms[..., 0] = np.maximum(correlograms[..., 0], correlograms[..., 0].T) sym = correlograms[..., 1:][..., ::-1] sym = np.transpose(sym, (1, 0, 2)) @@ -73,12 +69,18 @@ def firing_rate(spike_clusters, cluster_ids=None, bin_size=None, duration=None): n = len(cluster_ids) - len(bc) bc = np.concatenate((bc, np.zeros(n, dtype=bc.dtype))) assert bc.shape == (len(cluster_ids),) - return bc * np.c_[bc] * (bin_size / (duration or 1.)) + return bc * np.c_[bc] * (bin_size / (duration or 1.0)) def correlograms( - spike_times, spike_clusters, cluster_ids=None, sample_rate=1., - bin_size=None, window_size=None, symmetrize=True): + spike_times, + spike_clusters, + cluster_ids=None, + sample_rate=1.0, + bin_size=None, + window_size=None, + symmetrize=True, +): """Compute all pairwise cross-correlograms among the clusters appearing in `spike_clusters`. @@ -108,9 +110,8 @@ def correlograms( A `(n_clusters, n_clusters, winsize_samples)` array with all pairwise CCGs. """ - assert sample_rate > 0. - assert np.all(np.diff(spike_times) >= 0), ("The spike times must be " - "increasing.") + assert sample_rate > 0.0 + assert np.all(np.diff(spike_times) >= 0), 'The spike times must be increasing.' # Get the spike samples. spike_times = np.asarray(spike_times, dtype=np.float64) @@ -128,7 +129,7 @@ def correlograms( # Find `winsize_bins`. window_size = np.clip(window_size, 1e-5, 1e5) # in seconds - winsize_bins = 2 * int(.5 * window_size / bin_size) + 1 + winsize_bins = 2 * int(0.5 * window_size / bin_size) + 1 assert winsize_bins >= 1 assert winsize_bins % 2 == 1 @@ -177,7 +178,9 @@ def correlograms( # Find the indices in the raveled correlograms array that need # to be incremented, taking into account the spike clusters. indices = np.ravel_multi_index( - (spike_clusters_i[:-shift][m], spike_clusters_i[+shift:][m], d), correlograms.shape) + (spike_clusters_i[:-shift][m], spike_clusters_i[+shift:][m], d), + correlograms.shape, + ) # Increment the matching spikes in the correlograms array. _increment(correlograms.ravel(), indices) diff --git a/phylib/stats/clusters.py b/phylib/stats/clusters.py index 07bf50d..2a35a7d 100644 --- a/phylib/stats/clusters.py +++ b/phylib/stats/clusters.py @@ -1,24 +1,22 @@ -# -*- coding: utf-8 -*- - """Cluster statistics.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import numpy as np - -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Cluster statistics -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def mean(x): """Return the mean of an array across the first dimension.""" return x.mean(axis=0) -def get_unmasked_channels(mean_masks, min_mask=.25): +def get_unmasked_channels(mean_masks, min_mask=0.25): """Return the unmasked channels (mean masks above a given threshold).""" return np.nonzero(mean_masks > min_mask)[0] @@ -32,14 +30,14 @@ def get_mean_probe_position(mean_masks, site_positions): def get_sorted_main_channels(mean_masks, unmasked_channels): """Weighted mean of the channels, weighted by the mean masks.""" main_channels = np.argsort(mean_masks)[::-1] - main_channels = np.array([c for c in main_channels - if c in unmasked_channels]) + main_channels = np.array([c for c in main_channels if c in unmasked_channels]) return main_channels -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Wizard measures -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def get_waveform_amplitude(mean_masks, mean_waveforms): """Return the amplitude of the waveforms on all channels.""" @@ -59,7 +57,12 @@ def get_waveform_amplitude(mean_masks, mean_waveforms): def get_mean_masked_features_distance( - mean_features_0, mean_features_1, mean_masks_0, mean_masks_1, n_features_per_channel=None): + mean_features_0, + mean_features_1, + mean_masks_0, + mean_masks_1, + n_features_per_channel=None, +): """Compute the distance between the mean masked features.""" assert n_features_per_channel > 0 diff --git a/phylib/stats/tests/test_ccg.py b/phylib/stats/tests/test_ccg.py index 8b46636..8a6cba8 100644 --- a/phylib/stats/tests/test_ccg.py +++ b/phylib/stats/tests/test_ccg.py @@ -1,36 +1,35 @@ -# -*- coding: utf-8 -*- - """Tests of CCG functions.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import numpy as np from numpy.testing import assert_array_equal as ae -from ..ccg import (_increment, - _diff_shifted, - correlograms, - firing_rate, - ) - +from ..ccg import ( + _diff_shifted, + _increment, + correlograms, + firing_rate, +) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Tests -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _random_data(max_cluster): sr = 20000 nspikes = 10000 - spike_samples = np.cumsum(np.random.exponential(scale=.025, size=nspikes)) + spike_samples = np.cumsum(np.random.exponential(scale=0.025, size=nspikes)) spike_samples = (spike_samples * sr).astype(np.uint64) spike_clusters = np.random.randint(0, max_cluster, nspikes) return spike_samples, spike_clusters def _ccg_params(): - return .001, .05 + return 0.001, 0.05 def test_utils(): @@ -60,7 +59,7 @@ def test_firing_rate_0(): bin_size = 1 fr = firing_rate(spike_clusters, bin_size=bin_size, duration=20) - ae(fr, .2 * np.ones((2, 2))) + ae(fr, 0.2 * np.ones((2, 2))) def test_firing_rate_1(): @@ -69,14 +68,14 @@ def test_firing_rate_1(): fr = firing_rate(spike_clusters, cluster_ids=[0, 1, 2], bin_size=bin_size, duration=20) print(fr) - ae(fr[:2, :2], .2 * np.ones((2, 2))) + ae(fr[:2, :2], 0.2 * np.ones((2, 2))) assert np.all(fr[2, :] == fr[:, 2]) assert np.all(fr[2, :] == 0) def test_firing_rate_2(): spike_clusters = np.tile(np.arange(10), 100) - fr = firing_rate(spike_clusters, cluster_ids=np.arange(10), bin_size=.1, duration=1.) + fr = firing_rate(spike_clusters, cluster_ids=np.arange(10), bin_size=0.1, duration=1.0) ae(fr, np.ones((10, 10)) * 1000) @@ -94,9 +93,14 @@ def test_ccg_0(): c_expected[1, 0, 0] = 1 c_expected[0, 1, 0] = 0 # This is a peculiarity of the algorithm. - c = correlograms(spike_samples, spike_clusters, - bin_size=bin_size, window_size=winsize_bins, - cluster_ids=[0, 1], symmetrize=False) + c = correlograms( + spike_samples, + spike_clusters, + bin_size=bin_size, + window_size=winsize_bins, + cluster_ids=[0, 1], + symmetrize=False, + ) ae(c, c_expected) @@ -111,9 +115,13 @@ def test_ccg_1(): c_expected[0, 1, 1] = 1 c_expected[0, 0, 2] = 1 - c = correlograms(spike_samples, spike_clusters, - bin_size=bin_size, window_size=winsize_bins, - symmetrize=False) + c = correlograms( + spike_samples, + spike_clusters, + bin_size=bin_size, + window_size=winsize_bins, + symmetrize=False, + ) ae(c, c_expected) @@ -124,8 +132,13 @@ def test_ccg_2(): bin_size, winsize_bins = _ccg_params() c = correlograms( - spike_samples, spike_clusters, bin_size=bin_size, window_size=winsize_bins, - sample_rate=20000, symmetrize=False) + spike_samples, + spike_clusters, + bin_size=bin_size, + window_size=winsize_bins, + sample_rate=20000, + symmetrize=False, + ) assert c.shape == (max_cluster, max_cluster, 26) @@ -136,17 +149,26 @@ def test_ccg_symmetry_time(): spike_samples, spike_clusters = _random_data(2) bin_size, winsize_bins = _ccg_params() - c0 = correlograms(spike_samples, spike_clusters, - bin_size=bin_size, window_size=winsize_bins, - sample_rate=20000, symmetrize=False) + c0 = correlograms( + spike_samples, + spike_clusters, + bin_size=bin_size, + window_size=winsize_bins, + sample_rate=20000, + symmetrize=False, + ) - spike_samples_1 = np.cumsum(np.r_[np.arange(1), - np.diff(spike_samples)[::-1]]) + spike_samples_1 = np.cumsum(np.r_[np.arange(1), np.diff(spike_samples)[::-1]]) spike_samples_1 = spike_samples_1.astype(np.uint64) spike_clusters_1 = spike_clusters[::-1] - c1 = correlograms(spike_samples_1, spike_clusters_1, - bin_size=bin_size, window_size=winsize_bins, - sample_rate=20000, symmetrize=False) + c1 = correlograms( + spike_samples_1, + spike_clusters_1, + bin_size=bin_size, + window_size=winsize_bins, + sample_rate=20000, + symmetrize=False, + ) # The ACGs are identical. ae(c0[0, 0], c1[0, 0]) @@ -163,14 +185,24 @@ def test_ccg_symmetry_clusters(): spike_samples, spike_clusters = _random_data(2) bin_size, winsize_bins = _ccg_params() - c0 = correlograms(spike_samples, spike_clusters, - bin_size=bin_size, window_size=winsize_bins, - sample_rate=20000, symmetrize=False) + c0 = correlograms( + spike_samples, + spike_clusters, + bin_size=bin_size, + window_size=winsize_bins, + sample_rate=20000, + symmetrize=False, + ) spike_clusters_1 = 1 - spike_clusters - c1 = correlograms(spike_samples, spike_clusters_1, - bin_size=bin_size, window_size=winsize_bins, - sample_rate=20000, symmetrize=False) + c1 = correlograms( + spike_samples, + spike_clusters_1, + bin_size=bin_size, + window_size=winsize_bins, + sample_rate=20000, + symmetrize=False, + ) # The ACGs are identical. ae(c0[0, 0], c1[1, 1]) @@ -185,9 +217,13 @@ def test_symmetrize_correlograms(): spike_samples, spike_clusters = _random_data(3) bin_size, winsize_bins = _ccg_params() - sym = correlograms(spike_samples, spike_clusters, - bin_size=bin_size, window_size=winsize_bins, - sample_rate=20000) + sym = correlograms( + spike_samples, + spike_clusters, + bin_size=bin_size, + window_size=winsize_bins, + sample_rate=20000, + ) assert sym.shape == (3, 3, 51) # The ACG are reversed. diff --git a/phylib/stats/tests/test_clusters.py b/phylib/stats/tests/test_clusters.py index 5ed097a..52cdef0 100644 --- a/phylib/stats/tests/test_clusters.py +++ b/phylib/stats/tests/test_clusters.py @@ -1,30 +1,30 @@ -# -*- coding: utf-8 -*- - """Tests of cluster statistics.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import numpy as np -from numpy.testing import assert_array_equal as ae from numpy.testing import assert_allclose as ac +from numpy.testing import assert_array_equal as ae from pytest import fixture -from ..clusters import (mean, - get_unmasked_channels, - get_mean_probe_position, - get_sorted_main_channels, - get_mean_masked_features_distance, - get_waveform_amplitude, - ) -from phylib.utils.geometry import staggered_positions from phylib.io.mock import artificial_features, artificial_masks, artificial_waveforms +from phylib.utils.geometry import staggered_positions +from ..clusters import ( + get_mean_masked_features_distance, + get_mean_probe_position, + get_sorted_main_channels, + get_unmasked_channels, + get_waveform_amplitude, + mean, +) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Fixtures -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + @fixture def n_channels(): @@ -66,9 +66,10 @@ def site_positions(n_channels): yield staggered_positions(n_channels) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Tests -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_mean(features, n_channels, n_features_per_channel): mf = mean(features) @@ -78,7 +79,7 @@ def test_mean(features, n_channels, n_features_per_channel): def test_unmasked_channels(masks, n_channels): # Mask many values in the masks array. - threshold = .05 + threshold = 0.05 masks[:, 1::2] *= threshold # Compute the mean masks. mean_masks = mean(masks) @@ -89,7 +90,7 @@ def test_unmasked_channels(masks, n_channels): def test_mean_probe_position(masks, site_positions): - masks[:, ::2] *= .05 + masks[:, ::2] *= 0.05 mean_masks = mean(masks) mean_pos = get_mean_probe_position(mean_masks, site_positions) assert mean_pos.shape == (2,) @@ -98,17 +99,16 @@ def test_mean_probe_position(masks, site_positions): def test_sorted_main_channels(masks): - masks *= .05 + masks *= 0.05 masks[:, [5, 7]] *= 20 mean_masks = mean(masks) - channels = get_sorted_main_channels(mean_masks, - get_unmasked_channels(mean_masks)) + channels = get_sorted_main_channels(mean_masks, get_unmasked_channels(mean_masks)) assert np.all(np.isin(channels, [5, 7])) def test_waveform_amplitude(masks, waveforms): - waveforms *= .1 - masks *= .1 + waveforms *= 0.1 + masks *= 0.1 waveforms[:, 10, :] *= 10 masks[:, 10] *= 10 @@ -121,13 +121,13 @@ def test_waveform_amplitude(masks, waveforms): assert amplitude.shape == (mean_waveforms.shape[1],) -def test_mean_masked_features_distance(features, - n_channels, - n_features_per_channel, - ): - +def test_mean_masked_features_distance( + features, + n_channels, + n_features_per_channel, +): # Shifted feature vectors. - shift = 10. + shift = 10.0 f0 = mean(features) f1 = mean(features) + shift @@ -137,6 +137,5 @@ def test_mean_masked_features_distance(features, # Check the distance. d_expected = np.sqrt(n_features_per_channel) * shift - d_computed = get_mean_masked_features_distance(f0, f1, m0, m1, - n_features_per_channel) + d_computed = get_mean_masked_features_distance(f0, f1, m0, m1, n_features_per_channel) ac(d_expected, d_computed) diff --git a/phylib/utils/__init__.py b/phylib/utils/__init__.py index bbdd9ec..cd084f8 100644 --- a/phylib/utils/__init__.py +++ b/phylib/utils/__init__.py @@ -4,9 +4,26 @@ """Utilities.""" from ._misc import ( - load_json, save_json, load_pickle, save_pickle, _fullname, read_python, - read_text, write_text, read_tsv, write_tsv) + load_json, + save_json, + load_pickle, + save_pickle, + _fullname, + read_python, + read_text, + write_text, + read_tsv, + write_tsv, +) from ._types import ( - _is_array_like, _as_array, _as_tuple, _as_list, _as_scalar, _as_scalars, - Bunch, _is_list, _bunchify) + _is_array_like, + _as_array, + _as_tuple, + _as_list, + _as_scalar, + _as_scalars, + Bunch, + _is_list, + _bunchify, +) from .event import ProgressReporter, emit, connect, unconnect, silent, reset, set_silent diff --git a/phylib/utils/_misc.py b/phylib/utils/_misc.py index 4c78072..4befef5 100644 --- a/phylib/utils/_misc.py +++ b/phylib/utils/_misc.py @@ -1,20 +1,18 @@ -# -*- coding: utf-8 -*- - """Utility functions.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import base64 import csv -from importlib import import_module import json import logging import os -from pathlib import Path import subprocess +from importlib import import_module +from pathlib import Path from textwrap import dedent import numpy as np @@ -24,9 +22,10 @@ logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # JSON utility functions -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _encode_qbytearray(arr): """Encode binary arrays with base64.""" @@ -39,15 +38,22 @@ def _decode_qbytearray(data_b64): """Decode binary arrays with base64.""" encoded = base64.b64decode(data_b64) try: - from PyQt5.QtCore import QByteArray + from PyQt6.QtCore import QByteArray + out = QByteArray.fromBase64(encoded) - except ImportError: # pragma: no cover - pass + except ImportError: + try: + from PyQt5.QtCore import QByteArray + + out = QByteArray.fromBase64(encoded) + except ImportError: # pragma: no cover + out = None return out class _CustomEncoder(json.JSONEncoder): """JSON encoder that accepts NumPy arrays.""" + def default(self, obj): if isinstance(obj, np.ndarray) and obj.ndim == 1 and obj.shape[0] <= 10: # Serialize small arrays in clear text (lists of numbers). @@ -55,12 +61,16 @@ def default(self, obj): elif isinstance(obj, np.ndarray): obj_contiguous = np.ascontiguousarray(obj) data_b64 = base64.b64encode(obj_contiguous.data).decode('utf8') - return dict(__ndarray__=data_b64, dtype=str(obj.dtype), shape=obj.shape) + return { + '__ndarray__': data_b64, + 'dtype': str(obj.dtype), + 'shape': obj.shape, + } elif obj.__class__.__name__ == 'QByteArray': return {'__qbytearray__': _encode_qbytearray(obj)} elif isinstance(obj, np.generic): return obj.item() - return super(_CustomEncoder, self).default(obj) # pragma: no cover + return super().default(obj) # pragma: no cover def _json_custom_hook(d): @@ -97,10 +107,10 @@ def _stringify_keys(d): def _pretty_floats(obj, n=2): """Display floating point numbers properly.""" - if isinstance(obj, (float, np.float64, np.float32)): - return ('%.' + str(n) + 'f') % obj + if isinstance(obj, (float, np.floating)): + return (f'%.{str(n)}f') % obj elif isinstance(obj, dict): - return dict((k, _pretty_floats(v)) for k, v in obj.items()) + return {k: _pretty_floats(v) for k, v in obj.items()} elif isinstance(obj, (list, tuple)): return list(map(_pretty_floats, obj)) return obj @@ -110,7 +120,7 @@ def load_json(path): """Load a JSON file.""" path = Path(path) if not path.exists(): - raise IOError("The JSON file `{}` doesn't exist.".format(path)) + raise OSError(f"The JSON file `{path}` doesn't exist.") contents = path.read_text() if not contents: return {} @@ -134,19 +144,22 @@ def save_json(path, data): json.dump(data, f, cls=_CustomEncoder, indent=2, sort_keys=True) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Other read/write functions -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def load_pickle(path): """Load a pickle file using joblib.""" from joblib import load + return load(path) def save_pickle(path, data): """Save data to a pickle file using joblib.""" from joblib import dump + return dump(data, path) @@ -167,7 +180,7 @@ def read_python(path): """ path = Path(path) if not path.exists(): # pragma: no cover - raise IOError("Path %s does not exist.", path) + raise OSError('Path %s does not exist.', path) contents = path.read_text() metadata = {} exec(contents, {}, metadata) @@ -193,8 +206,8 @@ def write_python(path, data): with open(path, 'w') as f: for k, v in data.items(): if isinstance(v, str): - v = '"%s"' % v - f.write('%s = %s\n' % (k, str(v))) + v = f'"{v}"' + f.write(f'{k} = {str(v)}\n') def read_text(path): @@ -235,7 +248,7 @@ def read_tsv(path): path = Path(path) data = [] if not path.exists(): - logger.debug("%s does not exist, skipping.", path) + logger.debug('%s does not exist, skipping.', path) return data # Find whether the delimiter is tab or comma. with path.open('r') as f: @@ -246,7 +259,7 @@ def read_tsv(path): field_names = list(next(reader)) for row in reader: data.append({k: _try_make_number(v) for k, v in zip(field_names, row) if v != ''}) - logger.log(5, "Read %s.", path) + logger.log(5, 'Read %s.', path) return data @@ -270,7 +283,7 @@ def write_tsv(path, data, first_field=None, exclude_fields=(), n_significant_fig delimiter = '\t' if path.suffix == '.tsv' else ',' with path.open('w', newline='') as f: if not data: - logger.info("Data was empty when writing %s.", path) + logger.info('Data was empty when writing %s.', path) return # Get the union of all keys from all rows. fields = set().union(*data) @@ -289,9 +302,12 @@ def write_tsv(path, data, first_field=None, exclude_fields=(), n_significant_fig writer.writerow(fields) # Write all rows. writer.writerows( - [[_pretty_floats(row.get(field, None), n_significant_figures) - for field in fields] for row in data]) - logger.debug("Wrote %s.", path) + [ + [_pretty_floats(row.get(field, None), n_significant_figures) for field in fields] + for row in data + ] + ) + logger.debug('Wrote %s.', path) def _read_tsv_simple(path): @@ -303,7 +319,7 @@ def _read_tsv_simple(path): path = Path(path) data = {} if not path.exists(): - logger.debug("%s does not exist, skipping.", path) + logger.debug('%s does not exist, skipping.', path) return data # Find whether the delimiter is tab or comma. with path.open('r') as f: @@ -316,7 +332,7 @@ def _read_tsv_simple(path): cluster_id, value = row cluster_id = int(cluster_id) data[cluster_id] = _try_make_number(value) - logger.debug("Read %s.", path) + logger.debug('Read %s.', path) return field_name, data @@ -333,16 +349,17 @@ def _write_tsv_simple(path, field_name, data): writer = csv.writer(f, delimiter=delimiter) writer.writerow(['cluster_id', field_name]) writer.writerows([(cluster_id, data[cluster_id]) for cluster_id in sorted(data)]) - logger.debug("Wrote %s.", path) + logger.debug('Wrote %s.', path) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Various Python utility functions -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _fullname(o): """Return the fully-qualified name of a function.""" - return o.__module__ + "." + o.__name__ if o.__module__ else o.__name__ + return f'{o.__module__}.{o.__name__}' if o.__module__ else o.__name__ def _load_from_fullname(name): @@ -359,12 +376,13 @@ def _git_version(): os.chdir(str(Path(__file__).parent)) try: with open(os.devnull, 'w') as fnull: - version = ('-git-' + subprocess.check_output( - ['git', 'describe', '--abbrev=8', '--dirty', '--always', '--tags'], - stderr=fnull).strip().decode('ascii')) + version = '-git-' + subprocess.check_output( + ['git', 'describe', '--abbrev=8', '--dirty', '--always', '--tags'], + stderr=fnull, + ).strip().decode('ascii') return version except (OSError, subprocess.CalledProcessError): # pragma: no cover - return "" + return '' finally: os.chdir(curdir) diff --git a/phylib/utils/_types.py b/phylib/utils/_types.py index 3881d6b..b001d07 100644 --- a/phylib/utils/_types.py +++ b/phylib/utils/_types.py @@ -1,33 +1,43 @@ -# -*- coding: utf-8 -*- - """Utility functions.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import numpy as np - -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Various Python utility functions -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ _ACCEPTED_ARRAY_DTYPES = ( - float, np.float32, np.float64, int, np.int8, np.int16, np.uint8, np.uint16, - np.int32, np.int64, np.uint32, np.uint64, bool) + float, + np.float32, + np.float64, + int, + np.int8, + np.int16, + np.uint8, + np.uint16, + np.int32, + np.int64, + np.uint32, + np.uint64, + bool, +) class Bunch(dict): """A subclass of dictionary with an additional dot syntax.""" + def __init__(self, *args, **kwargs): - super(Bunch, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.__dict__ = self def copy(self): """Return a new Bunch instance which is a copy of the current Bunch instance.""" - return Bunch(super(Bunch, self).copy()) + return Bunch(super().copy()) def _bunchify(b): @@ -60,12 +70,12 @@ def _as_scalars(arr): def _is_integer(x): """Return whether an object is an integer.""" - return isinstance(x, (int, np.generic)) + return isinstance(x, (int, np.integer)) def _is_float(x): """Return whether an object is a floating point number.""" - return isinstance(x, (float, np.float32, np.float64)) + return isinstance(x, (float, np.floating)) def _as_list(obj): @@ -104,8 +114,7 @@ def _as_array(arr, dtype=None): if out.dtype != dtype: out = out.astype(dtype) if out.dtype not in _ACCEPTED_ARRAY_DTYPES: - raise ValueError("'arr' seems to have an invalid dtype: " - "{0:s}".format(str(out.dtype))) + raise ValueError(f"'arr' seems to have an invalid dtype: {str(out.dtype):s}") return out diff --git a/phylib/utils/event.py b/phylib/utils/event.py index 4e15fe5..e3e736f 100644 --- a/phylib/utils/event.py +++ b/phylib/utils/event.py @@ -1,26 +1,24 @@ -# -*- coding: utf-8 -*- -from __future__ import print_function - """Simple event system.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ -from contextlib import contextmanager import logging -import string import re +import string +from contextlib import contextmanager from functools import partial logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Event system -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ -class EventEmitter(object): + +class EventEmitter: """Singleton class that emits events and accepts registered callbacks. Example @@ -56,12 +54,11 @@ def reset(self): def _get_on_name(self, func): """Return `eventname` when the function name is `on_()`.""" - r = re.match("^on_(.+)$", func.__name__) + r = re.match('^on_(.+)$', func.__name__) if r: event = r.group(1) else: - raise ValueError("The function name should be " - "`on_`().") + raise ValueError('The function name should be `on_`().') return event @contextmanager @@ -69,9 +66,9 @@ def silent(self): """Prevent all callbacks to be called if events are raised in the context manager. """ - self.is_silent = not(self.is_silent) + self.is_silent = not (self.is_silent) yield - self.is_silent = not(self.is_silent) + self.is_silent = not (self.is_silent) def connect(self, func=None, event=None, sender=None, **kwargs): """Register a callback function to a given event. @@ -109,8 +106,8 @@ def unconnect(self, *items): self._callbacks = [ (event, sender, f, kwargs) for (event, sender, f, kwargs) in self._callbacks - if f not in items and sender not in items and - getattr(f, '__self__', None) not in items] + if f not in items and sender not in items and getattr(f, '__self__', None) not in items + ] def emit(self, event, sender, *args, **kwargs): """Call all callback functions registered with an event. @@ -125,8 +122,13 @@ def emit(self, event, sender, *args, **kwargs): return sender_name = sender.__class__.__name__ logger.log( - 5, "Emit %s.%s(%s, %s)", sender_name, event, - ', '.join(map(str, args)), ', '.join('%s=%s' % (k, v) for k, v in kwargs.items())) + 5, + 'Emit %s.%s(%s, %s)', + sender_name, + event, + ', '.join(map(str, args)), + ', '.join(f'{k}={v}' for k, v in kwargs.items()), + ) # Call the last callback if this is a single event. single = kwargs.pop('single', None) res = [] @@ -137,24 +139,24 @@ def emit(self, event, sender, *args, **kwargs): if e == event and (s is None or s == sender): f_name = getattr(f, '__qualname__', getattr(f, '__name__', str(f))) s_name = s.__class__.__name__ - logger.log(5, "Callback %s (%s).", f_name, s_name) + logger.log(5, 'Callback %s (%s).', f_name, s_name) res.append(f(sender, *args, **kwargs)) if single: return res[-1] return res -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Progress reporter -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + class PartialFormatter(string.Formatter): """Prevent KeyError when a format parameter is absent.""" + def get_field(self, field_name, args, kwargs): try: - return super(PartialFormatter, self).get_field(field_name, - args, - kwargs) + return super().get_field(field_name, args, kwargs) except (KeyError, AttributeError): return None, field_name @@ -163,7 +165,7 @@ def format_field(self, value, spec): if value is None: return '?' try: - return super(PartialFormatter, self).format_field(value, spec) + return super().format_field(value, spec) except ValueError: return '?' @@ -183,10 +185,10 @@ def _default_on_complete(message, end='\n', **kwargs): # Override the initializing message and clear the terminal # line. fmt = PartialFormatter() - print(fmt.format(message + '\033[K', **kwargs), end=end) + print(fmt.format(f'{message}\x1b[K', **kwargs), end=end) -class ProgressReporter(object): +class ProgressReporter: """A class that reports progress done. Example @@ -212,8 +214,9 @@ class ProgressReporter(object): * `complete()` """ + def __init__(self): - super(ProgressReporter, self).__init__() + super().__init__() self._value = 0 self._value_max = 0 self._has_completed = False @@ -297,9 +300,9 @@ def progress(self): return self._value / float(self._value_max) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Global event system -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ _EVENT = EventEmitter() diff --git a/phylib/utils/geometry.py b/phylib/utils/geometry.py index 1e32237..e085e54 100644 --- a/phylib/utils/geometry.py +++ b/phylib/utils/geometry.py @@ -1,28 +1,26 @@ -# -*- coding: utf-8 -*- - """Plotting utilities.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ -from functools import partial import logging +from functools import partial import numpy as np logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Common probe layouts -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def linear_positions(n_channels): """Linear channel positions along the vertical axis.""" - return np.c_[np.zeros(n_channels), - np.linspace(0., 1., n_channels)] + return np.c_[np.zeros(n_channels), np.linspace(0.0, 1.0, n_channels)] def staggered_positions(n_channels): @@ -33,9 +31,10 @@ def staggered_positions(n_channels): return pos -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Box positioning -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def range_transform(from_bounds, to_bounds, positions, do_offset=True): """Transform for a rectangle to another.""" @@ -57,7 +56,7 @@ def range_transform(from_bounds, to_bounds, positions, do_offset=True): z0[ind, i] = -1 z1[ind, i] = +1 - d = (f1 - f0) + d = f1 - f0 d[d == 0] = 1 out = positions.copy() @@ -73,17 +72,17 @@ def _boxes_overlap(x0, y0, x1, y1): """Return whether a set of boxes, defined by their 2D corners, overlap or not.""" assert x0.ndim == y0.ndim == y0.ndim == y1.ndim == 2 n = len(x0) - overlap_matrix = ((x0 < x1.T) & (x1 > x0.T) & (y0 < y1.T) & (y1 > y0.T)) + overlap_matrix = (x0 < x1.T) & (x1 > x0.T) & (y0 < y1.T) & (y1 > y0.T) overlap_matrix[np.arange(n), np.arange(n)] = False return np.any(overlap_matrix.ravel()) def _binary_search(f, xmin, xmax, eps=1e-9): """Return the largest x such f(x) is True.""" - middle = (xmax + xmin) / 2. + middle = (xmax + xmin) / 2.0 while xmax - xmin > eps: assert xmin < xmax - middle = (xmax + xmin) / 2. + middle = (xmax + xmin) / 2.0 if f(xmax): return xmax if not f(xmin): @@ -95,19 +94,19 @@ def _binary_search(f, xmin, xmax, eps=1e-9): return middle -def _find_box_size(x, y, ar=.5, margin=0): +def _find_box_size(x, y, ar=0.5, margin=0): """Return the maximum (half) box size such that boxes centered around box positions do not overlap.""" if x.ndim == 1: x = x[:, np.newaxis] if y.ndim == 1: y = y[:, np.newaxis] - logger.log(5, "Get box size for %d points.", len(x)) + logger.log(5, 'Get box size for %d points.', len(x)) # Deal with degenerate x case. xmin, xmax = x.min(), x.max() if xmin == xmax: # If all positions are vertical, the width can be maximum. - wmax = 1. + wmax = 1.0 else: wmax = xmax - xmin @@ -143,7 +142,7 @@ def get_non_overlapping_boxes(box_pos): box_pos = range_transform([[mx, my, Mx, My]], [[-1, -1, +1, +1]], box_pos) # Compute box size. x, y = box_pos.T - w, h = _find_box_size(x, y, margin=.1) + w, h = _find_box_size(x, y, margin=0.1) # Renormalize again so that the boxes fit inside the view. mx, my = np.min(box_pos - np.array([[w, h]]), axis=0) Mx, My = np.max(box_pos + np.array([[w, h]]), axis=0) @@ -151,9 +150,9 @@ def get_non_overlapping_boxes(box_pos): b2 = [[-1, -1, 1, 1]] box_pos = range_transform(b1, b2, box_pos) w, h = range_transform(b1, b2, [[w, h]], do_offset=False).ravel() - w *= .95 - h *= .9 - logger.log(5, "Found box size %s.", (w, h)) + w *= 0.95 + h *= 0.9 + logger.log(5, 'Found box size %s.', (w, h)) return box_pos, (w, h) @@ -170,12 +169,13 @@ def get_closest_box(pos, box_pos, box_size): return np.argmin(d) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Data bounds utilities -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _get_data_bounds(data_bounds, pos=None, length=None): - """"Prepare data bounds, possibly using min/max of the data.""" + """ "Prepare data bounds, possibly using min/max of the data.""" if data_bounds is None or (isinstance(data_bounds, str) and data_bounds == 'auto'): if pos is not None and len(pos): m, M = pos.min(axis=0), pos.max(axis=0) diff --git a/phylib/utils/testing.py b/phylib/utils/testing.py index 59d3d83..e961e96 100644 --- a/phylib/utils/testing.py +++ b/phylib/utils/testing.py @@ -1,28 +1,27 @@ -# -*- coding: utf-8 -*- - """Utility functions used for tests.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ -from contextlib import contextmanager -from io import StringIO import logging import os import sys +from contextlib import contextmanager +from io import StringIO -from numpy.testing import assert_array_equal as ae from numpy.testing import assert_allclose as ac +from numpy.testing import assert_array_equal as ae from ._types import _is_array_like logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Utility functions -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + @contextmanager def captured_output(): diff --git a/phylib/utils/tests/test_event.py b/phylib/utils/tests/test_event.py index e1236c7..a403a48 100644 --- a/phylib/utils/tests/test_event.py +++ b/phylib/utils/tests/test_event.py @@ -1,19 +1,17 @@ -# -*- coding: utf-8 -*- - """Test event system.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ from pytest import raises from ..event import EventEmitter, ProgressReporter, connect - -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test event system -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_event_system(): ev = EventEmitter() @@ -78,9 +76,10 @@ def on_test(sender): assert l == [0, 1, 0] -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test progress reporter -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_progress_reporter(): """Test the progress reporter.""" @@ -102,19 +101,19 @@ def on_complete(sender): pr.value = 0 pr.value = 5 assert pr.value == 5 - assert pr.progress == .5 + assert pr.progress == 0.5 assert not pr.is_complete() pr.value = 10 assert pr.is_complete() - assert pr.progress == 1. + assert pr.progress == 1.0 assert _completed == [True] pr.value_max = 11 assert not pr.is_complete() - assert pr.progress < 1. + assert pr.progress < 1.0 pr.set_complete() assert pr.is_complete() - assert pr.progress == 1. + assert pr.progress == 1.0 assert _reported == [(0, 10), (5, 10), (10, 10), (11, 11)] assert _completed == [True, True] @@ -130,8 +129,8 @@ def test_progress_message(): """Test messages with the progress reporter.""" pr = ProgressReporter() pr.reset(5) - pr.set_progress_message("The progress is {progress}%. ({hello:d})") - pr.set_complete_message("Finished {hello}.") + pr.set_progress_message('The progress is {progress}%. ({hello:d})') + pr.set_complete_message('Finished {hello}.') pr.value_max = 10 pr.value = 0 diff --git a/phylib/utils/tests/test_geometry.py b/phylib/utils/tests/test_geometry.py index cdd542e..117b81b 100644 --- a/phylib/utils/tests/test_geometry.py +++ b/phylib/utils/tests/test_geometry.py @@ -1,39 +1,35 @@ -# -*- coding: utf-8 -*- - """Test geometry utilities.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import numpy as np -from numpy.testing import assert_array_equal as ae -from numpy.testing import assert_allclose as ac import pytest +from numpy.testing import assert_allclose as ac +from numpy.testing import assert_array_equal as ae from ..geometry import ( - linear_positions, - staggered_positions, - _get_data_bounds, - _boxes_overlap, _binary_search, + _boxes_overlap, _find_box_size, - get_non_overlapping_boxes, + _get_data_bounds, get_closest_box, + get_non_overlapping_boxes, + linear_positions, + staggered_positions, ) - -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test utilities -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_get_data_bounds(): - ae(_get_data_bounds(None), [[-1., -1., 1., 1.]]) + ae(_get_data_bounds(None), [[-1.0, -1.0, 1.0, 1.0]]) - db0 = np.array([[0, 1, 4, 5], - [0, 1, 4, 5], - [0, 1, 4, 5]]) + db0 = np.array([[0, 1, 4, 5], [0, 1, 4, 5], [0, 1, 4, 5]]) arr = np.arange(6).reshape((3, 2)) assert np.all(_get_data_bounds(None, arr) == [[0, 1, 4, 5]]) @@ -52,7 +48,6 @@ def test_get_data_bounds(): def test_boxes_overlap(): - def _get_args(boxes): x0, y0, x1, y1 = np.array(boxes).T x0 = x0[:, np.newaxis] @@ -70,28 +65,29 @@ def _get_args(boxes): assert _boxes_overlap(x0, y0, x1, y1) x = np.zeros((5, 1)) - x0 = x - .1 - x1 = x + .1 + x0 = x - 0.1 + x1 = x + 0.1 y = np.linspace(-1, 1, 5)[:, np.newaxis] - y0 = y - .2 - y1 = y + .2 + y0 = y - 0.2 + y1 = y + 0.2 assert not _boxes_overlap(x0, y0, x1, y1) def test_binary_search(): def f(x): - return x < .4 - ac(_binary_search(f, 0, 1), .4) - ac(_binary_search(f, 0, .3), .3) - ac(_binary_search(f, .5, 1), .5) + return x < 0.4 + + ac(_binary_search(f, 0, 1), 0.4) + ac(_binary_search(f, 0, 0.3), 0.3) + ac(_binary_search(f, 0.5, 1), 0.5) def test_find_box_size(): x = np.zeros(5) y = np.linspace(-1, 1, 5) w, h = _find_box_size(x, y, margin=0) - ac(w, .5, atol=1e-8) - ac(h, .25, atol=1e-8) + ac(w, 0.5, atol=1e-8) + ac(h, 0.25, atol=1e-8) @pytest.mark.parametrize('n_channels', [5, 500]) @@ -102,7 +98,7 @@ def test_get_non_overlapping_boxes_1(n_channels): ac(box_pos[:, 0], 0, atol=1e-8) ac(box_pos[:, 1], -box_pos[::-1, 1], atol=1e-8) - assert box_size[0] >= .8 + assert box_size[0] >= 0.8 s = np.array([box_size]) box_bounds = np.c_[box_pos - s, box_pos + s] @@ -113,7 +109,7 @@ def test_get_non_overlapping_boxes_1(n_channels): def test_get_non_overlapping_boxes_2(): pos = staggered_positions(32) box_pos, box_size = get_non_overlapping_boxes(pos) - assert box_size[0] >= .05 + assert box_size[0] >= 0.05 s = np.array([box_size]) box_bounds = np.c_[box_pos - s, box_pos + s] @@ -126,17 +122,19 @@ def test_get_closest_box(): px = np.zeros(n) py = np.linspace(-1, 1, n) box_pos = np.c_[px, py] - w, h = (1, .9 / n) + w, h = (1, 0.9 / n) expected = [] for x in (0, -1, 1, -2, +2): for i in range(n): - expected.extend([ - (x, py[i], i), - (x, py[i] - h, i), - (x, py[i] + h, i), - (x, py[i] - 1.25 * h, max(0, min(i - 1, n - 1))), - (x, py[i] + 1.25 * h, max(0, min(i + 1, n - 1))), - ]) + expected.extend( + [ + (x, py[i], i), + (x, py[i] - h, i), + (x, py[i] + h, i), + (x, py[i] - 1.25 * h, max(0, min(i - 1, n - 1))), + (x, py[i] + 1.25 * h, max(0, min(i + 1, n - 1))), + ] + ) for x, y, i in expected: assert get_closest_box((x, y), box_pos, (w, h)) == i diff --git a/phylib/utils/tests/test_misc.py b/phylib/utils/tests/test_misc.py index 4885812..99fa70c 100644 --- a/phylib/utils/tests/test_misc.py +++ b/phylib/utils/tests/test_misc.py @@ -1,24 +1,38 @@ -# -*- coding: utf-8 -*- - """Tests of misc utility functions.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import numpy as np from numpy.testing import assert_array_equal as ae -from pytest import raises, mark +from pytest import mark, raises from .._misc import ( - _git_version, load_json, save_json, load_pickle, save_pickle, read_python, write_python, - read_text, write_text, _read_tsv_simple, _write_tsv_simple, read_tsv, write_tsv, - _pretty_floats, _encode_qbytearray, _decode_qbytearray, _fullname, _load_from_fullname) - - -#------------------------------------------------------------------------------ + _decode_qbytearray, + _encode_qbytearray, + _fullname, + _git_version, + _load_from_fullname, + _pretty_floats, + _read_tsv_simple, + _write_tsv_simple, + load_json, + load_pickle, + read_python, + read_text, + read_tsv, + save_json, + save_pickle, + write_python, + write_text, + write_tsv, +) + +# ------------------------------------------------------------------------------ # Misc tests -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_qbytearray(tempdir): try: @@ -60,7 +74,7 @@ def test_json_simple(tempdir): path.write_text('') assert load_json(path) == {} with raises(IOError): - load_json('%s_bis' % path) + load_json(f'{path}_bis') @mark.parametrize('kind', ['json', 'pickle']) @@ -101,9 +115,10 @@ def test_write_python(tempdir): def test_write_text(tempdir): - for path in (tempdir / 'test_1', - tempdir / 'test_dir/test_2.txt', - ): + for path in ( + tempdir / 'test_1', + tempdir / 'test_dir/test_2.txt', + ): write_text(path, 'hello world') assert read_text(path) == 'hello world' diff --git a/phylib/utils/tests/test_testing.py b/phylib/utils/tests/test_testing.py index dedb7f5..66bcaa6 100644 --- a/phylib/utils/tests/test_testing.py +++ b/phylib/utils/tests/test_testing.py @@ -1,33 +1,32 @@ -# -*- coding: utf-8 -*- - """Tests of testing utility functions.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ -from copy import deepcopy import logging +from copy import deepcopy import numpy as np -from ..testing import captured_output, captured_logging, _assert_equal +from ..testing import _assert_equal, captured_logging, captured_output logger = logging.getLogger('phylib') -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Tests -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_logging_1(): print() logger.setLevel(5) - logger.log(5, "level 5") - logger.log(10, "debug") - logger.log(20, "info") - logger.log(30, "warning") - logger.log(40, "error") + logger.log(5, 'level 5') + logger.log(10, 'debug') + logger.log(20, 'info') + logger.log(30, 'warning') + logger.log(40, 'error') def test_captured_output(): @@ -45,7 +44,7 @@ def test_captured_logging(): def test_assert_equal(): - d = {'a': {'b': np.random.rand(5), 3: 'c'}, 'b': 2.} + d = {'a': {'b': np.random.rand(5), 3: 'c'}, 'b': 2.0} d_bis = deepcopy(d) d_bis['a']['b'] = d_bis['a']['b'] + 1e-16 _assert_equal(d, d_bis) diff --git a/phylib/utils/tests/test_types.py b/phylib/utils/tests/test_types.py index 4bf9a17..313125e 100644 --- a/phylib/utils/tests/test_types.py +++ b/phylib/utils/tests/test_types.py @@ -1,23 +1,30 @@ -# -*- coding: utf-8 -*- - """Tests of misc type utility functions.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import numpy as np from pytest import raises -from .._types import (Bunch, _bunchify, _is_integer, _is_list, _is_float, - _as_list, _is_array_like, _as_array, _as_tuple, - _as_scalar, _as_scalars, - ) - - -#------------------------------------------------------------------------------ +from .._types import ( + Bunch, + _as_array, + _as_list, + _as_scalar, + _as_scalars, + _as_tuple, + _bunchify, + _is_array_like, + _is_float, + _is_integer, + _is_list, +) + +# ------------------------------------------------------------------------------ # Tests -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_bunch(): obj = Bunch() @@ -37,15 +44,15 @@ def test_bunchify(): def test_number(): assert not _is_integer(None) - assert not _is_integer(3.) + assert not _is_integer(3.0) assert _is_integer(3) assert _is_integer(np.arange(1)[0]) assert not _is_float(None) assert not _is_float(3) assert not _is_float(np.array([3])[0]) - assert _is_float(3.) - assert _is_float(np.array([3.])[0]) + assert _is_float(3.0) + assert _is_float(np.array([3.0])[0]) def test_list(): @@ -67,14 +74,14 @@ def test_as_tuple(): assert _as_tuple(None) is None assert _as_tuple((None,)) == (None,) assert _as_tuple((3, 4)) == (3, 4) - assert _as_tuple([3]) == ([3], ) - assert _as_tuple([3, 4]) == ([3, 4], ) + assert _as_tuple([3]) == ([3],) + assert _as_tuple([3, 4]) == ([3, 4],) def test_as_scalar(): assert _as_scalar(1) == 1 - assert _as_scalar(np.ones(1)[0]) == 1. - assert type(_as_scalar(np.ones(1)[0])) == float + assert _as_scalar(np.ones(1)[0]) == 1.0 + assert isinstance(_as_scalar(np.ones(1)[0]), float) assert _as_scalars(np.arange(3)) == [0, 1, 2] @@ -85,11 +92,11 @@ def _check(arr): assert np.all(arr == [3]) _check(_as_array(3)) - _check(_as_array(3.)) + _check(_as_array(3.0)) _check(_as_array([3])) _check(_as_array(3, float)) - _check(_as_array(3., float)) + _check(_as_array(3.0, float)) _check(_as_array([3], float)) _check(_as_array(np.array([3]))) with raises(ValueError): diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6c1de2c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,143 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "phylib" +dynamic = ["version"] +description = "Ephys data analysis for thousands of channels" +readme = "README.md" +license = { text = "BSD" } +authors = [{ name = "Cyrille Rossant", email = "cyrille.rossant@gmail.com" }] +keywords = ["phy", "data analysis", "electrophysiology", "neuroscience"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Natural Language :: English", + "Framework :: IPython", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +requires-python = ">=3.9" +dependencies = [ + "numpy", + "scipy", + "dask", + "requests", + "tqdm", + "toolz", + "joblib", + "mtscomp", + "responses>=0.25.7", +] + + +[project.urls] +Homepage = "https://github.com/cortex-lab/phylib" +Repository = "https://github.com/cortex-lab/phylib" + +[project.optional-dependencies] +dev = ["pytest", "pytest-cov", "ruff", "coverage", "coveralls", "responses"] +# Qt5 support (default for older systems) +qt5 = ["PyQt5>=5.12.0", "PyQtWebEngine>=5.12.0"] + +# Qt6 support (recommended for new installations) +qt6 = ["pyqt6>=6.9.1", "pyqt6-webengine>=6.9.0"] + +[tool.setuptools.dynamic] +version = { attr = "phylib.__version__" } + +[tool.setuptools.packages.find] +include = ["phylib*"] + +[tool.setuptools.package-data] +phylib = [ + "*.vert", + "*.frag", + "*.glsl", + "*.npy", + "*.gz", + "*.txt", + "*.html", + "*.css", + "*.js", + "*.prb", +] + +[tool.pytest.ini_options] +filterwarnings = [ + "default", + "ignore::DeprecationWarning:responses", + "ignore::DeprecationWarning:cookies", + "ignore::DeprecationWarning:socks", + "ignore::DeprecationWarning:matplotlib", + "ignore:numpy.ufunc", +] +testpaths = ["phylib"] +addopts = "--cov=phylib --cov-report=term-missing" + +[tool.coverage.run] +branch = false +source = ["phylib"] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "raise", + "except IOError:", + "pass", + "return$", + "continue$", +] +show_missing = true + +[tool.ruff] +line-length = 99 +target-version = "py39" + +[tool.ruff.lint] +select = ["E", "F", "W", "I", "N", "UP", "B", "C4", "SIM", "PIE", "NPY201"] +ignore = [ + "E265", # block comment should start with '# ' + "E731", # do not assign a lambda expression, use a def + "E741", # ambiguous variable name + "W605", # invalid escape sequence, + "N806", + "SIM102", + "B007", + "N803", + "N802", + "B018", + "F401", + "SIM118", + "B015", + "C416", + "E402", + "E501", + "SIM108", + "SIM115", + "UP028", +] + +[tool.ruff.lint.isort] +known-first-party = ["phylib"] + +[tool.ruff.format] +quote-style = "single" +indent-style = "space" + +[tool.uv] +dev-dependencies = [ + "pytest>=6.0", + "pytest-qt>=4.0", + "pytest-cov>=3.0", + "ruff>=0.1.0", + "coverage>=6.0", + "coveralls>=3.0", + "memory_profiler>=0.60", + "mkdocs>=1.4", +] diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..d8e4962 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1447 @@ +version = 1 +revision = 2 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version < '3.10'", +] + +[[package]] +name = "certifi" +version = "2025.4.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" }, + { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" }, + { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" }, + { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" }, + { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" }, + { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" }, + { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, + { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, + { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, + { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, + { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/28/f8/dfb01ff6cc9af38552c69c9027501ff5a5117c4cc18dcd27cb5259fa1888/charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", size = 201671, upload-time = "2025-05-02T08:34:12.696Z" }, + { url = "https://files.pythonhosted.org/packages/32/fb/74e26ee556a9dbfe3bd264289b67be1e6d616329403036f6507bb9f3f29c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", size = 144744, upload-time = "2025-05-02T08:34:14.665Z" }, + { url = "https://files.pythonhosted.org/packages/ad/06/8499ee5aa7addc6f6d72e068691826ff093329fe59891e83b092ae4c851c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", size = 154993, upload-time = "2025-05-02T08:34:17.134Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a2/5e4c187680728219254ef107a6949c60ee0e9a916a5dadb148c7ae82459c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", size = 147382, upload-time = "2025-05-02T08:34:19.081Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/56aca740dda674f0cc1ba1418c4d84534be51f639b5f98f538b332dc9a95/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", size = 149536, upload-time = "2025-05-02T08:34:21.073Z" }, + { url = "https://files.pythonhosted.org/packages/53/13/db2e7779f892386b589173dd689c1b1e304621c5792046edd8a978cbf9e0/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", size = 151349, upload-time = "2025-05-02T08:34:23.193Z" }, + { url = "https://files.pythonhosted.org/packages/69/35/e52ab9a276186f729bce7a0638585d2982f50402046e4b0faa5d2c3ef2da/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", size = 146365, upload-time = "2025-05-02T08:34:25.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d8/af7333f732fc2e7635867d56cb7c349c28c7094910c72267586947561b4b/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", size = 154499, upload-time = "2025-05-02T08:34:27.359Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3d/a5b2e48acef264d71e036ff30bcc49e51bde80219bb628ba3e00cf59baac/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", size = 157735, upload-time = "2025-05-02T08:34:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/85/d8/23e2c112532a29f3eef374375a8684a4f3b8e784f62b01da931186f43494/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", size = 154786, upload-time = "2025-05-02T08:34:31.858Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/93e0169f08ecc20fe82d12254a200dfaceddc1c12a4077bf454ecc597e33/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", size = 150203, upload-time = "2025-05-02T08:34:33.88Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9d/9bf2b005138e7e060d7ebdec7503d0ef3240141587651f4b445bdf7286c2/charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", size = 98436, upload-time = "2025-05-02T08:34:35.907Z" }, + { url = "https://files.pythonhosted.org/packages/6d/24/5849d46cf4311bbf21b424c443b09b459f5b436b1558c04e45dbb7cc478b/charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", size = 105772, upload-time = "2025-05-02T08:34:37.935Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "cloudpickle" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/39/069100b84d7418bc358d81669d5748efb14b9cceacd2f9c75f550424132f/cloudpickle-3.1.1.tar.gz", hash = "sha256:b216fa8ae4019d5482a8ac3c95d8f6346115d8835911fd4aefd1a445e4242c64", size = 22113, upload-time = "2025-01-14T17:02:05.085Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/e8/64c37fadfc2816a7701fa8a6ed8d87327c7d54eacfbfb6edab14a2f2be75/cloudpickle-3.1.1-py3-none-any.whl", hash = "sha256:c8c5a44295039331ee9dad40ba100a9c7297b6f988e50e87ccdf3765a668350e", size = 20992, upload-time = "2025-01-14T17:02:02.417Z" }, +] + +[[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 = "coverage" +version = "7.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/07/998afa4a0ecdf9b1981ae05415dad2d4e7716e1b1f00abbd91691ac09ac9/coverage-7.8.2.tar.gz", hash = "sha256:a886d531373a1f6ff9fad2a2ba4a045b68467b779ae729ee0b3b10ac20033b27", size = 812759, upload-time = "2025-05-23T11:39:57.856Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/6b/7dd06399a5c0b81007e3a6af0395cd60e6a30f959f8d407d3ee04642e896/coverage-7.8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bd8ec21e1443fd7a447881332f7ce9d35b8fbd2849e761bb290b584535636b0a", size = 211573, upload-time = "2025-05-23T11:37:47.207Z" }, + { url = "https://files.pythonhosted.org/packages/f0/df/2b24090820a0bac1412955fb1a4dade6bc3b8dcef7b899c277ffaf16916d/coverage-7.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c26c2396674816deaeae7ded0e2b42c26537280f8fe313335858ffff35019be", size = 212006, upload-time = "2025-05-23T11:37:50.289Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c4/e4e3b998e116625562a872a342419652fa6ca73f464d9faf9f52f1aff427/coverage-7.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1aec326ed237e5880bfe69ad41616d333712c7937bcefc1343145e972938f9b3", size = 241128, upload-time = "2025-05-23T11:37:52.229Z" }, + { url = "https://files.pythonhosted.org/packages/b1/67/b28904afea3e87a895da850ba587439a61699bf4b73d04d0dfd99bbd33b4/coverage-7.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e818796f71702d7a13e50c70de2a1924f729228580bcba1607cccf32eea46e6", size = 239026, upload-time = "2025-05-23T11:37:53.846Z" }, + { url = "https://files.pythonhosted.org/packages/8c/0f/47bf7c5630d81bc2cd52b9e13043685dbb7c79372a7f5857279cc442b37c/coverage-7.8.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:546e537d9e24efc765c9c891328f30f826e3e4808e31f5d0f87c4ba12bbd1622", size = 240172, upload-time = "2025-05-23T11:37:55.711Z" }, + { url = "https://files.pythonhosted.org/packages/ba/38/af3eb9d36d85abc881f5aaecf8209383dbe0fa4cac2d804c55d05c51cb04/coverage-7.8.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab9b09a2349f58e73f8ebc06fac546dd623e23b063e5398343c5270072e3201c", size = 240086, upload-time = "2025-05-23T11:37:57.724Z" }, + { url = "https://files.pythonhosted.org/packages/9e/64/c40c27c2573adeba0fe16faf39a8aa57368a1f2148865d6bb24c67eadb41/coverage-7.8.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fd51355ab8a372d89fb0e6a31719e825cf8df8b6724bee942fb5b92c3f016ba3", size = 238792, upload-time = "2025-05-23T11:37:59.737Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ab/b7c85146f15457671c1412afca7c25a5696d7625e7158002aa017e2d7e3c/coverage-7.8.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0774df1e093acb6c9e4d58bce7f86656aeed6c132a16e2337692c12786b32404", size = 239096, upload-time = "2025-05-23T11:38:01.693Z" }, + { url = "https://files.pythonhosted.org/packages/d3/50/9446dad1310905fb1dc284d60d4320a5b25d4e3e33f9ea08b8d36e244e23/coverage-7.8.2-cp310-cp310-win32.whl", hash = "sha256:00f2e2f2e37f47e5f54423aeefd6c32a7dbcedc033fcd3928a4f4948e8b96af7", size = 214144, upload-time = "2025-05-23T11:38:03.68Z" }, + { url = "https://files.pythonhosted.org/packages/23/ed/792e66ad7b8b0df757db8d47af0c23659cdb5a65ef7ace8b111cacdbee89/coverage-7.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:145b07bea229821d51811bf15eeab346c236d523838eda395ea969d120d13347", size = 215043, upload-time = "2025-05-23T11:38:05.217Z" }, + { url = "https://files.pythonhosted.org/packages/6a/4d/1ff618ee9f134d0de5cc1661582c21a65e06823f41caf801aadf18811a8e/coverage-7.8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b99058eef42e6a8dcd135afb068b3d53aff3921ce699e127602efff9956457a9", size = 211692, upload-time = "2025-05-23T11:38:08.485Z" }, + { url = "https://files.pythonhosted.org/packages/96/fa/c3c1b476de96f2bc7a8ca01a9f1fcb51c01c6b60a9d2c3e66194b2bdb4af/coverage-7.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5feb7f2c3e6ea94d3b877def0270dff0947b8d8c04cfa34a17be0a4dc1836879", size = 212115, upload-time = "2025-05-23T11:38:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c2/5414c5a1b286c0f3881ae5adb49be1854ac5b7e99011501f81c8c1453065/coverage-7.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:670a13249b957bb9050fab12d86acef7bf8f6a879b9d1a883799276e0d4c674a", size = 244740, upload-time = "2025-05-23T11:38:11.947Z" }, + { url = "https://files.pythonhosted.org/packages/cd/46/1ae01912dfb06a642ef3dd9cf38ed4996fda8fe884dab8952da616f81a2b/coverage-7.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bdc8bf760459a4a4187b452213e04d039990211f98644c7292adf1e471162b5", size = 242429, upload-time = "2025-05-23T11:38:13.955Z" }, + { url = "https://files.pythonhosted.org/packages/06/58/38c676aec594bfe2a87c7683942e5a30224791d8df99bcc8439fde140377/coverage-7.8.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07a989c867986c2a75f158f03fdb413128aad29aca9d4dbce5fc755672d96f11", size = 244218, upload-time = "2025-05-23T11:38:15.631Z" }, + { url = "https://files.pythonhosted.org/packages/80/0c/95b1023e881ce45006d9abc250f76c6cdab7134a1c182d9713878dfefcb2/coverage-7.8.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2db10dedeb619a771ef0e2949ccba7b75e33905de959c2643a4607bef2f3fb3a", size = 243865, upload-time = "2025-05-23T11:38:17.622Z" }, + { url = "https://files.pythonhosted.org/packages/57/37/0ae95989285a39e0839c959fe854a3ae46c06610439350d1ab860bf020ac/coverage-7.8.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e6ea7dba4e92926b7b5f0990634b78ea02f208d04af520c73a7c876d5a8d36cb", size = 242038, upload-time = "2025-05-23T11:38:19.966Z" }, + { url = "https://files.pythonhosted.org/packages/4d/82/40e55f7c0eb5e97cc62cbd9d0746fd24e8caf57be5a408b87529416e0c70/coverage-7.8.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ef2f22795a7aca99fc3c84393a55a53dd18ab8c93fb431004e4d8f0774150f54", size = 242567, upload-time = "2025-05-23T11:38:21.912Z" }, + { url = "https://files.pythonhosted.org/packages/f9/35/66a51adc273433a253989f0d9cc7aa6bcdb4855382cf0858200afe578861/coverage-7.8.2-cp311-cp311-win32.whl", hash = "sha256:641988828bc18a6368fe72355df5f1703e44411adbe49bba5644b941ce6f2e3a", size = 214194, upload-time = "2025-05-23T11:38:23.571Z" }, + { url = "https://files.pythonhosted.org/packages/f6/8f/a543121f9f5f150eae092b08428cb4e6b6d2d134152c3357b77659d2a605/coverage-7.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:8ab4a51cb39dc1933ba627e0875046d150e88478dbe22ce145a68393e9652975", size = 215109, upload-time = "2025-05-23T11:38:25.137Z" }, + { url = "https://files.pythonhosted.org/packages/77/65/6cc84b68d4f35186463cd7ab1da1169e9abb59870c0f6a57ea6aba95f861/coverage-7.8.2-cp311-cp311-win_arm64.whl", hash = "sha256:8966a821e2083c74d88cca5b7dcccc0a3a888a596a04c0b9668a891de3a0cc53", size = 213521, upload-time = "2025-05-23T11:38:27.123Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2a/1da1ada2e3044fcd4a3254fb3576e160b8fe5b36d705c8a31f793423f763/coverage-7.8.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2f6fe3654468d061942591aef56686131335b7a8325684eda85dacdf311356c", size = 211876, upload-time = "2025-05-23T11:38:29.01Z" }, + { url = "https://files.pythonhosted.org/packages/70/e9/3d715ffd5b6b17a8be80cd14a8917a002530a99943cc1939ad5bb2aa74b9/coverage-7.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76090fab50610798cc05241bf83b603477c40ee87acd358b66196ab0ca44ffa1", size = 212130, upload-time = "2025-05-23T11:38:30.675Z" }, + { url = "https://files.pythonhosted.org/packages/a0/02/fdce62bb3c21649abfd91fbdcf041fb99be0d728ff00f3f9d54d97ed683e/coverage-7.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bd0a0a5054be160777a7920b731a0570284db5142abaaf81bcbb282b8d99279", size = 246176, upload-time = "2025-05-23T11:38:32.395Z" }, + { url = "https://files.pythonhosted.org/packages/a7/52/decbbed61e03b6ffe85cd0fea360a5e04a5a98a7423f292aae62423b8557/coverage-7.8.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da23ce9a3d356d0affe9c7036030b5c8f14556bd970c9b224f9c8205505e3b99", size = 243068, upload-time = "2025-05-23T11:38:33.989Z" }, + { url = "https://files.pythonhosted.org/packages/38/6c/d0e9c0cce18faef79a52778219a3c6ee8e336437da8eddd4ab3dbd8fadff/coverage-7.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9392773cffeb8d7e042a7b15b82a414011e9d2b5fdbbd3f7e6a6b17d5e21b20", size = 245328, upload-time = "2025-05-23T11:38:35.568Z" }, + { url = "https://files.pythonhosted.org/packages/f0/70/f703b553a2f6b6c70568c7e398ed0789d47f953d67fbba36a327714a7bca/coverage-7.8.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:876cbfd0b09ce09d81585d266c07a32657beb3eaec896f39484b631555be0fe2", size = 245099, upload-time = "2025-05-23T11:38:37.627Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fb/4cbb370dedae78460c3aacbdad9d249e853f3bc4ce5ff0e02b1983d03044/coverage-7.8.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3da9b771c98977a13fbc3830f6caa85cae6c9c83911d24cb2d218e9394259c57", size = 243314, upload-time = "2025-05-23T11:38:39.238Z" }, + { url = "https://files.pythonhosted.org/packages/39/9f/1afbb2cb9c8699b8bc38afdce00a3b4644904e6a38c7bf9005386c9305ec/coverage-7.8.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a990f6510b3292686713bfef26d0049cd63b9c7bb17e0864f133cbfd2e6167f", size = 244489, upload-time = "2025-05-23T11:38:40.845Z" }, + { url = "https://files.pythonhosted.org/packages/79/fa/f3e7ec7d220bff14aba7a4786ae47043770cbdceeea1803083059c878837/coverage-7.8.2-cp312-cp312-win32.whl", hash = "sha256:bf8111cddd0f2b54d34e96613e7fbdd59a673f0cf5574b61134ae75b6f5a33b8", size = 214366, upload-time = "2025-05-23T11:38:43.551Z" }, + { url = "https://files.pythonhosted.org/packages/54/aa/9cbeade19b7e8e853e7ffc261df885d66bf3a782c71cba06c17df271f9e6/coverage-7.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:86a323a275e9e44cdf228af9b71c5030861d4d2610886ab920d9945672a81223", size = 215165, upload-time = "2025-05-23T11:38:45.148Z" }, + { url = "https://files.pythonhosted.org/packages/c4/73/e2528bf1237d2448f882bbebaec5c3500ef07301816c5c63464b9da4d88a/coverage-7.8.2-cp312-cp312-win_arm64.whl", hash = "sha256:820157de3a589e992689ffcda8639fbabb313b323d26388d02e154164c57b07f", size = 213548, upload-time = "2025-05-23T11:38:46.74Z" }, + { url = "https://files.pythonhosted.org/packages/1a/93/eb6400a745ad3b265bac36e8077fdffcf0268bdbbb6c02b7220b624c9b31/coverage-7.8.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ea561010914ec1c26ab4188aef8b1567272ef6de096312716f90e5baa79ef8ca", size = 211898, upload-time = "2025-05-23T11:38:49.066Z" }, + { url = "https://files.pythonhosted.org/packages/1b/7c/bdbf113f92683024406a1cd226a199e4200a2001fc85d6a6e7e299e60253/coverage-7.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cb86337a4fcdd0e598ff2caeb513ac604d2f3da6d53df2c8e368e07ee38e277d", size = 212171, upload-time = "2025-05-23T11:38:51.207Z" }, + { url = "https://files.pythonhosted.org/packages/91/22/594513f9541a6b88eb0dba4d5da7d71596dadef6b17a12dc2c0e859818a9/coverage-7.8.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a4636ddb666971345541b59899e969f3b301143dd86b0ddbb570bd591f1e85", size = 245564, upload-time = "2025-05-23T11:38:52.857Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f4/2860fd6abeebd9f2efcfe0fd376226938f22afc80c1943f363cd3c28421f/coverage-7.8.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5040536cf9b13fb033f76bcb5e1e5cb3b57c4807fef37db9e0ed129c6a094257", size = 242719, upload-time = "2025-05-23T11:38:54.529Z" }, + { url = "https://files.pythonhosted.org/packages/89/60/f5f50f61b6332451520e6cdc2401700c48310c64bc2dd34027a47d6ab4ca/coverage-7.8.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc67994df9bcd7e0150a47ef41278b9e0a0ea187caba72414b71dc590b99a108", size = 244634, upload-time = "2025-05-23T11:38:57.326Z" }, + { url = "https://files.pythonhosted.org/packages/3b/70/7f4e919039ab7d944276c446b603eea84da29ebcf20984fb1fdf6e602028/coverage-7.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e6c86888fd076d9e0fe848af0a2142bf606044dc5ceee0aa9eddb56e26895a0", size = 244824, upload-time = "2025-05-23T11:38:59.421Z" }, + { url = "https://files.pythonhosted.org/packages/26/45/36297a4c0cea4de2b2c442fe32f60c3991056c59cdc3cdd5346fbb995c97/coverage-7.8.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:684ca9f58119b8e26bef860db33524ae0365601492e86ba0b71d513f525e7050", size = 242872, upload-time = "2025-05-23T11:39:01.049Z" }, + { url = "https://files.pythonhosted.org/packages/a4/71/e041f1b9420f7b786b1367fa2a375703889ef376e0d48de9f5723fb35f11/coverage-7.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8165584ddedb49204c4e18da083913bdf6a982bfb558632a79bdaadcdafd0d48", size = 244179, upload-time = "2025-05-23T11:39:02.709Z" }, + { url = "https://files.pythonhosted.org/packages/bd/db/3c2bf49bdc9de76acf2491fc03130c4ffc51469ce2f6889d2640eb563d77/coverage-7.8.2-cp313-cp313-win32.whl", hash = "sha256:34759ee2c65362163699cc917bdb2a54114dd06d19bab860725f94ef45a3d9b7", size = 214393, upload-time = "2025-05-23T11:39:05.457Z" }, + { url = "https://files.pythonhosted.org/packages/c6/dc/947e75d47ebbb4b02d8babb1fad4ad381410d5bc9da7cfca80b7565ef401/coverage-7.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:2f9bc608fbafaee40eb60a9a53dbfb90f53cc66d3d32c2849dc27cf5638a21e3", size = 215194, upload-time = "2025-05-23T11:39:07.171Z" }, + { url = "https://files.pythonhosted.org/packages/90/31/a980f7df8a37eaf0dc60f932507fda9656b3a03f0abf188474a0ea188d6d/coverage-7.8.2-cp313-cp313-win_arm64.whl", hash = "sha256:9fe449ee461a3b0c7105690419d0b0aba1232f4ff6d120a9e241e58a556733f7", size = 213580, upload-time = "2025-05-23T11:39:08.862Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6a/25a37dd90f6c95f59355629417ebcb74e1c34e38bb1eddf6ca9b38b0fc53/coverage-7.8.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8369a7c8ef66bded2b6484053749ff220dbf83cba84f3398c84c51a6f748a008", size = 212734, upload-time = "2025-05-23T11:39:11.109Z" }, + { url = "https://files.pythonhosted.org/packages/36/8b/3a728b3118988725f40950931abb09cd7f43b3c740f4640a59f1db60e372/coverage-7.8.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:159b81df53a5fcbc7d45dae3adad554fdbde9829a994e15227b3f9d816d00b36", size = 212959, upload-time = "2025-05-23T11:39:12.751Z" }, + { url = "https://files.pythonhosted.org/packages/53/3c/212d94e6add3a3c3f412d664aee452045ca17a066def8b9421673e9482c4/coverage-7.8.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6fcbbd35a96192d042c691c9e0c49ef54bd7ed865846a3c9d624c30bb67ce46", size = 257024, upload-time = "2025-05-23T11:39:15.569Z" }, + { url = "https://files.pythonhosted.org/packages/a4/40/afc03f0883b1e51bbe804707aae62e29c4e8c8bbc365c75e3e4ddeee9ead/coverage-7.8.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05364b9cc82f138cc86128dc4e2e1251c2981a2218bfcd556fe6b0fbaa3501be", size = 252867, upload-time = "2025-05-23T11:39:17.64Z" }, + { url = "https://files.pythonhosted.org/packages/18/a2/3699190e927b9439c6ded4998941a3c1d6fa99e14cb28d8536729537e307/coverage-7.8.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46d532db4e5ff3979ce47d18e2fe8ecad283eeb7367726da0e5ef88e4fe64740", size = 255096, upload-time = "2025-05-23T11:39:19.328Z" }, + { url = "https://files.pythonhosted.org/packages/b4/06/16e3598b9466456b718eb3e789457d1a5b8bfb22e23b6e8bbc307df5daf0/coverage-7.8.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4000a31c34932e7e4fa0381a3d6deb43dc0c8f458e3e7ea6502e6238e10be625", size = 256276, upload-time = "2025-05-23T11:39:21.077Z" }, + { url = "https://files.pythonhosted.org/packages/a7/d5/4b5a120d5d0223050a53d2783c049c311eea1709fa9de12d1c358e18b707/coverage-7.8.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:43ff5033d657cd51f83015c3b7a443287250dc14e69910577c3e03bd2e06f27b", size = 254478, upload-time = "2025-05-23T11:39:22.838Z" }, + { url = "https://files.pythonhosted.org/packages/ba/85/f9ecdb910ecdb282b121bfcaa32fa8ee8cbd7699f83330ee13ff9bbf1a85/coverage-7.8.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94316e13f0981cbbba132c1f9f365cac1d26716aaac130866ca812006f662199", size = 255255, upload-time = "2025-05-23T11:39:24.644Z" }, + { url = "https://files.pythonhosted.org/packages/50/63/2d624ac7d7ccd4ebbd3c6a9eba9d7fc4491a1226071360d59dd84928ccb2/coverage-7.8.2-cp313-cp313t-win32.whl", hash = "sha256:3f5673888d3676d0a745c3d0e16da338c5eea300cb1f4ada9c872981265e76d8", size = 215109, upload-time = "2025-05-23T11:39:26.722Z" }, + { url = "https://files.pythonhosted.org/packages/22/5e/7053b71462e970e869111c1853afd642212568a350eba796deefdfbd0770/coverage-7.8.2-cp313-cp313t-win_amd64.whl", hash = "sha256:2c08b05ee8d7861e45dc5a2cc4195c8c66dca5ac613144eb6ebeaff2d502e73d", size = 216268, upload-time = "2025-05-23T11:39:28.429Z" }, + { url = "https://files.pythonhosted.org/packages/07/69/afa41aa34147655543dbe96994f8a246daf94b361ccf5edfd5df62ce066a/coverage-7.8.2-cp313-cp313t-win_arm64.whl", hash = "sha256:1e1448bb72b387755e1ff3ef1268a06617afd94188164960dba8d0245a46004b", size = 214071, upload-time = "2025-05-23T11:39:30.55Z" }, + { url = "https://files.pythonhosted.org/packages/71/1e/388267ad9c6aa126438acc1ceafede3bb746afa9872e3ec5f0691b7d5efa/coverage-7.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:496948261eaac5ac9cf43f5d0a9f6eb7a6d4cb3bedb2c5d294138142f5c18f2a", size = 211566, upload-time = "2025-05-23T11:39:32.333Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a5/acc03e5cf0bba6357f5e7c676343de40fbf431bb1e115fbebf24b2f7f65e/coverage-7.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eacd2de0d30871eff893bab0b67840a96445edcb3c8fd915e6b11ac4b2f3fa6d", size = 211996, upload-time = "2025-05-23T11:39:34.512Z" }, + { url = "https://files.pythonhosted.org/packages/5b/a2/0fc0a9f6b7c24fa4f1d7210d782c38cb0d5e692666c36eaeae9a441b6755/coverage-7.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b039ffddc99ad65d5078ef300e0c7eed08c270dc26570440e3ef18beb816c1ca", size = 240741, upload-time = "2025-05-23T11:39:36.252Z" }, + { url = "https://files.pythonhosted.org/packages/e6/da/1c6ba2cf259710eed8916d4fd201dccc6be7380ad2b3b9f63ece3285d809/coverage-7.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e49824808d4375ede9dd84e9961a59c47f9113039f1a525e6be170aa4f5c34d", size = 238672, upload-time = "2025-05-23T11:39:38.03Z" }, + { url = "https://files.pythonhosted.org/packages/ac/51/c8fae0dc3ca421e6e2509503696f910ff333258db672800c3bdef256265a/coverage-7.8.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b069938961dfad881dc2f8d02b47645cd2f455d3809ba92a8a687bf513839787", size = 239769, upload-time = "2025-05-23T11:39:40.24Z" }, + { url = "https://files.pythonhosted.org/packages/59/8e/b97042ae92c59f40be0c989df090027377ba53f2d6cef73c9ca7685c26a6/coverage-7.8.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:de77c3ba8bb686d1c411e78ee1b97e6e0b963fb98b1637658dd9ad2c875cf9d7", size = 239555, upload-time = "2025-05-23T11:39:42.3Z" }, + { url = "https://files.pythonhosted.org/packages/47/35/b8893e682d6e96b1db2af5997fc13ef62219426fb17259d6844c693c5e00/coverage-7.8.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1676628065a498943bd3f64f099bb573e08cf1bc6088bbe33cf4424e0876f4b3", size = 237768, upload-time = "2025-05-23T11:39:44.069Z" }, + { url = "https://files.pythonhosted.org/packages/03/6c/023b0b9a764cb52d6243a4591dcb53c4caf4d7340445113a1f452bb80591/coverage-7.8.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8e1a26e7e50076e35f7afafde570ca2b4d7900a491174ca357d29dece5aacee7", size = 238757, upload-time = "2025-05-23T11:39:46.195Z" }, + { url = "https://files.pythonhosted.org/packages/03/ed/3af7e4d721bd61a8df7de6de9e8a4271e67f3d9e086454558fd9f48eb4f6/coverage-7.8.2-cp39-cp39-win32.whl", hash = "sha256:6782a12bf76fa61ad9350d5a6ef5f3f020b57f5e6305cbc663803f2ebd0f270a", size = 214166, upload-time = "2025-05-23T11:39:47.934Z" }, + { url = "https://files.pythonhosted.org/packages/9d/30/ee774b626773750dc6128354884652507df3c59d6aa8431526107e595227/coverage-7.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:1efa4166ba75ccefd647f2d78b64f53f14fb82622bc94c5a5cb0a622f50f1c9e", size = 215050, upload-time = "2025-05-23T11:39:50.252Z" }, + { url = "https://files.pythonhosted.org/packages/69/2f/572b29496d8234e4a7773200dd835a0d32d9e171f2d974f3fe04a9dbc271/coverage-7.8.2-pp39.pp310.pp311-none-any.whl", hash = "sha256:ec455eedf3ba0bbdf8f5a570012617eb305c63cb9f03428d39bf544cb2b94837", size = 203636, upload-time = "2025-05-23T11:39:52.002Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1a/0b9c32220ad694d66062f571cc5cedfa9997b64a591e8a500bb63de1bd40/coverage-7.8.2-py3-none-any.whl", hash = "sha256:726f32ee3713f7359696331a18daf0c3b3a70bb0ae71141b9d3c52be7c595e32", size = 203623, upload-time = "2025-05-23T11:39:53.846Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "coveralls" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "docopt" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/75/a454fb443eb6a053833f61603a432ffbd7dd6ae53a11159bacfadb9d6219/coveralls-4.0.1.tar.gz", hash = "sha256:7b2a0a2bcef94f295e3cf28dcc55ca40b71c77d1c2446b538e85f0f7bc21aa69", size = 12419, upload-time = "2024-05-15T12:56:14.297Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/e5/6708c75e2a4cfca929302d4d9b53b862c6dc65bd75e6933ea3d20016d41d/coveralls-4.0.1-py3-none-any.whl", hash = "sha256:7a6b1fa9848332c7b2221afb20f3df90272ac0167060f41b5fe90429b30b1809", size = 13599, upload-time = "2024-05-15T12:56:12.342Z" }, +] + +[[package]] +name = "dask" +version = "2024.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "cloudpickle", marker = "python_full_version < '3.10'" }, + { name = "fsspec", marker = "python_full_version < '3.10'" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "partd", marker = "python_full_version < '3.10'" }, + { name = "pyyaml", marker = "python_full_version < '3.10'" }, + { name = "toolz", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/2e/568a422d907745a3a897a732c83d05a3923d8cfa2511a6abea2a0e19994e/dask-2024.8.0.tar.gz", hash = "sha256:f1fec39373d2f101bc045529ad4e9b30e34e6eb33b7aa0fa7073aec7b1bf9eee", size = 9895684, upload-time = "2024-08-06T20:23:54.464Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/47/136a5dd68a33089f96f8aa1178ccd545d325ec9ab2bb42a3038711a935c0/dask-2024.8.0-py3-none-any.whl", hash = "sha256:250ea3df30d4a25958290eec4f252850091c6cfaed82d098179c3b25bba18309", size = 1233681, upload-time = "2024-08-06T20:23:42.258Z" }, +] + +[[package]] +name = "dask" +version = "2025.5.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "cloudpickle", marker = "python_full_version >= '3.10'" }, + { name = "fsspec", marker = "python_full_version >= '3.10'" }, + { name = "importlib-metadata", marker = "python_full_version >= '3.10' and python_full_version < '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "partd", marker = "python_full_version >= '3.10'" }, + { name = "pyyaml", marker = "python_full_version >= '3.10'" }, + { name = "toolz", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/29/05feb8e2531c46d763547c66b7f5deb39b53d99b3be1b4ddddbd1cec6567/dask-2025.5.1.tar.gz", hash = "sha256:979d9536549de0e463f4cab8a8c66c3a2ef55791cd740d07d9bf58fab1d1076a", size = 10969324, upload-time = "2025-05-20T19:54:30.688Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/30/53b0844a7a4c6b041b111b24ca15cc9b8661a86fe1f6aaeb2d0d7f0fb1f2/dask-2025.5.1-py3-none-any.whl", hash = "sha256:3b85fdaa5f6f989dde49da6008415b1ae996985ebdfb1e40de2c997d9010371d", size = 1474226, upload-time = "2025-05-20T19:54:20.309Z" }, +] + +[[package]] +name = "docopt" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/55/8f8cab2afd404cf578136ef2cc5dfb50baa1761b68c9da1fb1e4eed343c9/docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491", size = 25901, upload-time = "2014-06-16T11:18:57.406Z" } + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "fsspec" +version = "2025.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/f7/27f15d41f0ed38e8fcc488584b57e902b331da7f7c6dcda53721b15838fc/fsspec-2025.5.1.tar.gz", hash = "sha256:2e55e47a540b91843b755e83ded97c6e897fa0942b11490113f09e9c443c2475", size = 303033, upload-time = "2025-05-24T12:03:23.792Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/61/78c7b3851add1481b048b5fdc29067397a1784e2910592bc81bb3f608635/fsspec-2025.5.1-py3-none-any.whl", hash = "sha256:24d3a2e663d5fc735ab256263c4075f374a174c3410c0b25e5bd1970bceaa462", size = 199052, upload-time = "2025-05-24T12:03:21.66Z" }, +] + +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[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 = "joblib" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/fe/0f5a938c54105553436dbff7a61dc4fed4b1b2c98852f8833beaf4d5968f/joblib-1.5.1.tar.gz", hash = "sha256:f4f86e351f39fe3d0d32a9f2c3d8af1ee4cec285aafcb27003dda5205576b444", size = 330475, upload-time = "2025-05-23T12:04:37.097Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/4f/1195bbac8e0c2acc5f740661631d8d750dc38d4a32b23ee5df3cde6f4e0d/joblib-1.5.1-py3-none-any.whl", hash = "sha256:4719a31f054c7d766948dcd83e9613686b27114f190f717cec7eaa2084f8a74a", size = 307746, upload-time = "2025-05-23T12:04:35.124Z" }, +] + +[[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.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/15/222b423b0b88689c266d9eac4e61396fe2cc53464459d6a37618ac863b24/markdown-3.8.tar.gz", hash = "sha256:7df81e63f0df5c4b24b7d156eb81e4690595239b7d70937d0409f1b0de319c6f", size = 360906, upload-time = "2025-04-11T14:42:50.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/3f/afe76f8e2246ffbc867440cbcf90525264df0e658f8a5ca1f872b3f6192a/markdown-3.8-py3-none-any.whl", hash = "sha256:794a929b79c5af141ef5ab0f2f642d0f7b1872981250230e72682346f7cc90dc", size = 106210, upload-time = "2025-04-11T14:42:49.178Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344, upload-time = "2024-10-18T15:21:43.721Z" }, + { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389, upload-time = "2024-10-18T15:21:44.666Z" }, + { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607, upload-time = "2024-10-18T15:21:45.452Z" }, + { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728, upload-time = "2024-10-18T15:21:46.295Z" }, + { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826, upload-time = "2024-10-18T15:21:47.134Z" }, + { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843, upload-time = "2024-10-18T15:21:48.334Z" }, + { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219, upload-time = "2024-10-18T15:21:49.587Z" }, + { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946, upload-time = "2024-10-18T15:21:50.441Z" }, + { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063, upload-time = "2024-10-18T15:21:51.385Z" }, + { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506, upload-time = "2024-10-18T15:21:52.974Z" }, +] + +[[package]] +name = "memory-profiler" +version = "0.61.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "psutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/88/e1907e1ca3488f2d9507ca8b0ae1add7b1cd5d3ca2bc8e5b329382ea2c7b/memory_profiler-0.61.0.tar.gz", hash = "sha256:4e5b73d7864a1d1292fb76a03e82a3e78ef934d06828a698d9dada76da2067b0", size = 35935, upload-time = "2022-11-15T17:57:28.994Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/26/aaca612a0634ceede20682e692a6c55e35a94c21ba36b807cc40fe910ae1/memory_profiler-0.61.0-py3-none-any.whl", hash = "sha256:400348e61031e3942ad4d4109d18753b2fb08c2f6fb8290671c5513a34182d84", size = 31803, upload-time = "2022-11-15T17:57:27.031Z" }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, +] + +[[package]] +name = "mtscomp" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/ef/365e2dd214155b06d22622b3278de769d20e9e1d201538a941d62b609248/mtscomp-1.0.2.tar.gz", hash = "sha256:609c4fe5a0d00532c1452b10318a74e04add8e47c562aca216e7b40de0e4bf73", size = 15967, upload-time = "2021-05-11T11:32:31.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/12/449d679e3aef2dcadfb9b275e2809d87bfeb798c7e9a911ee4bae536e24a/mtscomp-1.0.2-py2.py3-none-any.whl", hash = "sha256:a00a6d46a6155af5bca44931ccf5045756ea8256db8fd452f5e0592b71b4db69", size = 16382, upload-time = "2021-05-11T11:32:29.676Z" }, +] + +[[package]] +name = "numpy" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78", size = 18902015, upload-time = "2024-08-26T20:19:40.945Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/91/3495b3237510f79f5d81f2508f9f13fea78ebfdf07538fc7444badda173d/numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece", size = 21165245, upload-time = "2024-08-26T20:04:14.625Z" }, + { url = "https://files.pythonhosted.org/packages/05/33/26178c7d437a87082d11019292dce6d3fe6f0e9026b7b2309cbf3e489b1d/numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04", size = 13738540, upload-time = "2024-08-26T20:04:36.784Z" }, + { url = "https://files.pythonhosted.org/packages/ec/31/cc46e13bf07644efc7a4bf68df2df5fb2a1a88d0cd0da9ddc84dc0033e51/numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66", size = 5300623, upload-time = "2024-08-26T20:04:46.491Z" }, + { url = "https://files.pythonhosted.org/packages/6e/16/7bfcebf27bb4f9d7ec67332ffebee4d1bf085c84246552d52dbb548600e7/numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b", size = 6901774, upload-time = "2024-08-26T20:04:58.173Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a3/561c531c0e8bf082c5bef509d00d56f82e0ea7e1e3e3a7fc8fa78742a6e5/numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd", size = 13907081, upload-time = "2024-08-26T20:05:19.098Z" }, + { url = "https://files.pythonhosted.org/packages/fa/66/f7177ab331876200ac7563a580140643d1179c8b4b6a6b0fc9838de2a9b8/numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318", size = 19523451, upload-time = "2024-08-26T20:05:47.479Z" }, + { url = "https://files.pythonhosted.org/packages/25/7f/0b209498009ad6453e4efc2c65bcdf0ae08a182b2b7877d7ab38a92dc542/numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8", size = 19927572, upload-time = "2024-08-26T20:06:17.137Z" }, + { url = "https://files.pythonhosted.org/packages/3e/df/2619393b1e1b565cd2d4c4403bdd979621e2c4dea1f8532754b2598ed63b/numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326", size = 14400722, upload-time = "2024-08-26T20:06:39.16Z" }, + { url = "https://files.pythonhosted.org/packages/22/ad/77e921b9f256d5da36424ffb711ae79ca3f451ff8489eeca544d0701d74a/numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97", size = 6472170, upload-time = "2024-08-26T20:06:50.361Z" }, + { url = "https://files.pythonhosted.org/packages/10/05/3442317535028bc29cf0c0dd4c191a4481e8376e9f0db6bcf29703cadae6/numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131", size = 15905558, upload-time = "2024-08-26T20:07:13.881Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cf/034500fb83041aa0286e0fb16e7c76e5c8b67c0711bb6e9e9737a717d5fe/numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448", size = 21169137, upload-time = "2024-08-26T20:07:45.345Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d9/32de45561811a4b87fbdee23b5797394e3d1504b4a7cf40c10199848893e/numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195", size = 13703552, upload-time = "2024-08-26T20:08:06.666Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ca/2f384720020c7b244d22508cb7ab23d95f179fcfff33c31a6eeba8d6c512/numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57", size = 5298957, upload-time = "2024-08-26T20:08:15.83Z" }, + { url = "https://files.pythonhosted.org/packages/0e/78/a3e4f9fb6aa4e6fdca0c5428e8ba039408514388cf62d89651aade838269/numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a", size = 6905573, upload-time = "2024-08-26T20:08:27.185Z" }, + { url = "https://files.pythonhosted.org/packages/a0/72/cfc3a1beb2caf4efc9d0b38a15fe34025230da27e1c08cc2eb9bfb1c7231/numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669", size = 13914330, upload-time = "2024-08-26T20:08:48.058Z" }, + { url = "https://files.pythonhosted.org/packages/ba/a8/c17acf65a931ce551fee11b72e8de63bf7e8a6f0e21add4c937c83563538/numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951", size = 19534895, upload-time = "2024-08-26T20:09:16.536Z" }, + { url = "https://files.pythonhosted.org/packages/ba/86/8767f3d54f6ae0165749f84648da9dcc8cd78ab65d415494962c86fac80f/numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9", size = 19937253, upload-time = "2024-08-26T20:09:46.263Z" }, + { url = "https://files.pythonhosted.org/packages/df/87/f76450e6e1c14e5bb1eae6836478b1028e096fd02e85c1c37674606ab752/numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15", size = 14414074, upload-time = "2024-08-26T20:10:08.483Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/0f0f328e1e59f73754f06e1adfb909de43726d4f24c6a3f8805f34f2b0fa/numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4", size = 6470640, upload-time = "2024-08-26T20:10:19.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/57/3a3f14d3a759dcf9bf6e9eda905794726b758819df4663f217d658a58695/numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc", size = 15910230, upload-time = "2024-08-26T20:10:43.413Z" }, + { url = "https://files.pythonhosted.org/packages/45/40/2e117be60ec50d98fa08c2f8c48e09b3edea93cfcabd5a9ff6925d54b1c2/numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b", size = 20895803, upload-time = "2024-08-26T20:11:13.916Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/1b8b8dee833f53cef3e0a3f69b2374467789e0bb7399689582314df02651/numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e", size = 13471835, upload-time = "2024-08-26T20:11:34.779Z" }, + { url = "https://files.pythonhosted.org/packages/7f/19/e2793bde475f1edaea6945be141aef6c8b4c669b90c90a300a8954d08f0a/numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c", size = 5038499, upload-time = "2024-08-26T20:11:43.902Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ff/ddf6dac2ff0dd50a7327bcdba45cb0264d0e96bb44d33324853f781a8f3c/numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c", size = 6633497, upload-time = "2024-08-26T20:11:55.09Z" }, + { url = "https://files.pythonhosted.org/packages/72/21/67f36eac8e2d2cd652a2e69595a54128297cdcb1ff3931cfc87838874bd4/numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692", size = 13621158, upload-time = "2024-08-26T20:12:14.95Z" }, + { url = "https://files.pythonhosted.org/packages/39/68/e9f1126d757653496dbc096cb429014347a36b228f5a991dae2c6b6cfd40/numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a", size = 19236173, upload-time = "2024-08-26T20:12:44.049Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e9/1f5333281e4ebf483ba1c888b1d61ba7e78d7e910fdd8e6499667041cc35/numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c", size = 19634174, upload-time = "2024-08-26T20:13:13.634Z" }, + { url = "https://files.pythonhosted.org/packages/71/af/a469674070c8d8408384e3012e064299f7a2de540738a8e414dcfd639996/numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded", size = 14099701, upload-time = "2024-08-26T20:13:34.851Z" }, + { url = "https://files.pythonhosted.org/packages/d0/3d/08ea9f239d0e0e939b6ca52ad403c84a2bce1bde301a8eb4888c1c1543f1/numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5", size = 6174313, upload-time = "2024-08-26T20:13:45.653Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b5/4ac39baebf1fdb2e72585c8352c56d063b6126be9fc95bd2bb5ef5770c20/numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a", size = 15606179, upload-time = "2024-08-26T20:14:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/43/c1/41c8f6df3162b0c6ffd4437d729115704bd43363de0090c7f913cfbc2d89/numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c", size = 21169942, upload-time = "2024-08-26T20:14:40.108Z" }, + { url = "https://files.pythonhosted.org/packages/39/bc/fd298f308dcd232b56a4031fd6ddf11c43f9917fbc937e53762f7b5a3bb1/numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd", size = 13711512, upload-time = "2024-08-26T20:15:00.985Z" }, + { url = "https://files.pythonhosted.org/packages/96/ff/06d1aa3eeb1c614eda245c1ba4fb88c483bee6520d361641331872ac4b82/numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b", size = 5306976, upload-time = "2024-08-26T20:15:10.876Z" }, + { url = "https://files.pythonhosted.org/packages/2d/98/121996dcfb10a6087a05e54453e28e58694a7db62c5a5a29cee14c6e047b/numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729", size = 6906494, upload-time = "2024-08-26T20:15:22.055Z" }, + { url = "https://files.pythonhosted.org/packages/15/31/9dffc70da6b9bbf7968f6551967fc21156207366272c2a40b4ed6008dc9b/numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1", size = 13912596, upload-time = "2024-08-26T20:15:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/b9/14/78635daab4b07c0930c919d451b8bf8c164774e6a3413aed04a6d95758ce/numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd", size = 19526099, upload-time = "2024-08-26T20:16:11.048Z" }, + { url = "https://files.pythonhosted.org/packages/26/4c/0eeca4614003077f68bfe7aac8b7496f04221865b3a5e7cb230c9d055afd/numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d", size = 19932823, upload-time = "2024-08-26T20:16:40.171Z" }, + { url = "https://files.pythonhosted.org/packages/f1/46/ea25b98b13dccaebddf1a803f8c748680d972e00507cd9bc6dcdb5aa2ac1/numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d", size = 14404424, upload-time = "2024-08-26T20:17:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a6/177dd88d95ecf07e722d21008b1b40e681a929eb9e329684d449c36586b2/numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa", size = 6476809, upload-time = "2024-08-26T20:17:13.553Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2b/7fc9f4e7ae5b507c1a3a21f0f15ed03e794c1242ea8a242ac158beb56034/numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73", size = 15911314, upload-time = "2024-08-26T20:17:36.72Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3b/df5a870ac6a3be3a86856ce195ef42eec7ae50d2a202be1f5a4b3b340e14/numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8", size = 21025288, upload-time = "2024-08-26T20:18:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/2c/97/51af92f18d6f6f2d9ad8b482a99fb74e142d71372da5d834b3a2747a446e/numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4", size = 6762793, upload-time = "2024-08-26T20:18:19.125Z" }, + { url = "https://files.pythonhosted.org/packages/12/46/de1fbd0c1b5ccaa7f9a005b66761533e2f6a3e560096682683a223631fe9/numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c", size = 19334885, upload-time = "2024-08-26T20:18:47.237Z" }, + { url = "https://files.pythonhosted.org/packages/cc/dc/d330a6faefd92b446ec0f0dfea4c3207bb1fef3c4771d19cf4543efd2c78/numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385", size = 15828784, upload-time = "2024-08-26T20:19:11.19Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "numpy" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/db/8e12381333aea300890829a0a36bfa738cac95475d88982d538725143fd9/numpy-2.3.0.tar.gz", hash = "sha256:581f87f9e9e9db2cba2141400e160e9dd644ee248788d6f90636eeb8fd9260a6", size = 20382813, upload-time = "2025-06-07T14:54:32.608Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/5f/df67435257d827eb3b8af66f585223dc2c3f2eb7ad0b50cb1dae2f35f494/numpy-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c3c9fdde0fa18afa1099d6257eb82890ea4f3102847e692193b54e00312a9ae9", size = 21199688, upload-time = "2025-06-07T14:36:52.067Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ce/aad219575055d6c9ef29c8c540c81e1c38815d3be1fe09cdbe53d48ee838/numpy-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46d16f72c2192da7b83984aa5455baee640e33a9f1e61e656f29adf55e406c2b", size = 14359277, upload-time = "2025-06-07T14:37:15.325Z" }, + { url = "https://files.pythonhosted.org/packages/29/6b/2d31da8e6d2ec99bed54c185337a87f8fbeccc1cd9804e38217e92f3f5e2/numpy-2.3.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a0be278be9307c4ab06b788f2a077f05e180aea817b3e41cebbd5aaf7bd85ed3", size = 5376069, upload-time = "2025-06-07T14:37:25.636Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2a/6c59a062397553ec7045c53d5fcdad44e4536e54972faa2ba44153bca984/numpy-2.3.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:99224862d1412d2562248d4710126355d3a8db7672170a39d6909ac47687a8a4", size = 6913057, upload-time = "2025-06-07T14:37:37.215Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5a/8df16f258d28d033e4f359e29d3aeb54663243ac7b71504e89deeb813202/numpy-2.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2393a914db64b0ead0ab80c962e42d09d5f385802006a6c87835acb1f58adb96", size = 14568083, upload-time = "2025-06-07T14:37:59.337Z" }, + { url = "https://files.pythonhosted.org/packages/0a/92/0528a563dfc2cdccdcb208c0e241a4bb500d7cde218651ffb834e8febc50/numpy-2.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:7729c8008d55e80784bd113787ce876ca117185c579c0d626f59b87d433ea779", size = 16929402, upload-time = "2025-06-07T14:38:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/e4/2f/e7a8c8d4a2212c527568d84f31587012cf5497a7271ea1f23332142f634e/numpy-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:06d4fb37a8d383b769281714897420c5cc3545c79dc427df57fc9b852ee0bf58", size = 15879193, upload-time = "2025-06-07T14:38:48.007Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c3/dada3f005953847fe35f42ac0fe746f6e1ea90b4c6775e4be605dcd7b578/numpy-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c39ec392b5db5088259c68250e342612db82dc80ce044cf16496cf14cf6bc6f8", size = 18665318, upload-time = "2025-06-07T14:39:15.794Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ae/3f448517dedefc8dd64d803f9d51a8904a48df730e00a3c5fb1e75a60620/numpy-2.3.0-cp311-cp311-win32.whl", hash = "sha256:ee9d3ee70d62827bc91f3ea5eee33153212c41f639918550ac0475e3588da59f", size = 6601108, upload-time = "2025-06-07T14:39:27.176Z" }, + { url = "https://files.pythonhosted.org/packages/8c/4a/556406d2bb2b9874c8cbc840c962683ac28f21efbc9b01177d78f0199ca1/numpy-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:43c55b6a860b0eb44d42341438b03513cf3879cb3617afb749ad49307e164edd", size = 13021525, upload-time = "2025-06-07T14:39:46.637Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ee/bf54278aef30335ffa9a189f869ea09e1a195b3f4b93062164a3b02678a7/numpy-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:2e6a1409eee0cb0316cb64640a49a49ca44deb1a537e6b1121dc7c458a1299a8", size = 10170327, upload-time = "2025-06-07T14:40:02.703Z" }, + { url = "https://files.pythonhosted.org/packages/89/59/9df493df81ac6f76e9f05cdbe013cdb0c9a37b434f6e594f5bd25e278908/numpy-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:389b85335838155a9076e9ad7f8fdba0827496ec2d2dc32ce69ce7898bde03ba", size = 20897025, upload-time = "2025-06-07T14:40:33.558Z" }, + { url = "https://files.pythonhosted.org/packages/2f/86/4ff04335901d6cf3a6bb9c748b0097546ae5af35e455ae9b962ebff4ecd7/numpy-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9498f60cd6bb8238d8eaf468a3d5bb031d34cd12556af53510f05fcf581c1b7e", size = 14129882, upload-time = "2025-06-07T14:40:55.034Z" }, + { url = "https://files.pythonhosted.org/packages/71/8d/a942cd4f959de7f08a79ab0c7e6cecb7431d5403dce78959a726f0f57aa1/numpy-2.3.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:622a65d40d8eb427d8e722fd410ac3ad4958002f109230bc714fa551044ebae2", size = 5110181, upload-time = "2025-06-07T14:41:04.4Z" }, + { url = "https://files.pythonhosted.org/packages/86/5d/45850982efc7b2c839c5626fb67fbbc520d5b0d7c1ba1ae3651f2f74c296/numpy-2.3.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:b9446d9d8505aadadb686d51d838f2b6688c9e85636a0c3abaeb55ed54756459", size = 6647581, upload-time = "2025-06-07T14:41:14.695Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c0/c871d4a83f93b00373d3eebe4b01525eee8ef10b623a335ec262b58f4dc1/numpy-2.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:50080245365d75137a2bf46151e975de63146ae6d79f7e6bd5c0e85c9931d06a", size = 14262317, upload-time = "2025-06-07T14:41:35.862Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f6/bc47f5fa666d5ff4145254f9e618d56e6a4ef9b874654ca74c19113bb538/numpy-2.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c24bb4113c66936eeaa0dc1e47c74770453d34f46ee07ae4efd853a2ed1ad10a", size = 16633919, upload-time = "2025-06-07T14:42:00.622Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b4/65f48009ca0c9b76df5f404fccdea5a985a1bb2e34e97f21a17d9ad1a4ba/numpy-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4d8d294287fdf685281e671886c6dcdf0291a7c19db3e5cb4178d07ccf6ecc67", size = 15567651, upload-time = "2025-06-07T14:42:24.429Z" }, + { url = "https://files.pythonhosted.org/packages/f1/62/5367855a2018578e9334ed08252ef67cc302e53edc869666f71641cad40b/numpy-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6295f81f093b7f5769d1728a6bd8bf7466de2adfa771ede944ce6711382b89dc", size = 18361723, upload-time = "2025-06-07T14:42:51.167Z" }, + { url = "https://files.pythonhosted.org/packages/d4/75/5baed8cd867eabee8aad1e74d7197d73971d6a3d40c821f1848b8fab8b84/numpy-2.3.0-cp312-cp312-win32.whl", hash = "sha256:e6648078bdd974ef5d15cecc31b0c410e2e24178a6e10bf511e0557eed0f2570", size = 6318285, upload-time = "2025-06-07T14:43:02.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/49/d5781eaa1a15acb3b3a3f49dc9e2ff18d92d0ce5c2976f4ab5c0a7360250/numpy-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:0898c67a58cdaaf29994bc0e2c65230fd4de0ac40afaf1584ed0b02cd74c6fdd", size = 12732594, upload-time = "2025-06-07T14:43:21.071Z" }, + { url = "https://files.pythonhosted.org/packages/c2/1c/6d343e030815c7c97a1f9fbad00211b47717c7fe446834c224bd5311e6f1/numpy-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:bd8df082b6c4695753ad6193018c05aac465d634834dca47a3ae06d4bb22d9ea", size = 9891498, upload-time = "2025-06-07T14:43:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/73/fc/1d67f751fd4dbafc5780244fe699bc4084268bad44b7c5deb0492473127b/numpy-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5754ab5595bfa2c2387d241296e0381c21f44a4b90a776c3c1d39eede13a746a", size = 20889633, upload-time = "2025-06-07T14:44:06.839Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/73ffdb69e5c3f19ec4530f8924c4386e7ba097efc94b9c0aff607178ad94/numpy-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d11fa02f77752d8099573d64e5fe33de3229b6632036ec08f7080f46b6649959", size = 14151683, upload-time = "2025-06-07T14:44:28.847Z" }, + { url = "https://files.pythonhosted.org/packages/64/d5/06d4bb31bb65a1d9c419eb5676173a2f90fd8da3c59f816cc54c640ce265/numpy-2.3.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:aba48d17e87688a765ab1cd557882052f238e2f36545dfa8e29e6a91aef77afe", size = 5102683, upload-time = "2025-06-07T14:44:38.417Z" }, + { url = "https://files.pythonhosted.org/packages/12/8b/6c2cef44f8ccdc231f6b56013dff1d71138c48124334aded36b1a1b30c5a/numpy-2.3.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4dc58865623023b63b10d52f18abaac3729346a7a46a778381e0e3af4b7f3beb", size = 6640253, upload-time = "2025-06-07T14:44:49.359Z" }, + { url = "https://files.pythonhosted.org/packages/62/aa/fca4bf8de3396ddb59544df9b75ffe5b73096174de97a9492d426f5cd4aa/numpy-2.3.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:df470d376f54e052c76517393fa443758fefcdd634645bc9c1f84eafc67087f0", size = 14258658, upload-time = "2025-06-07T14:45:10.156Z" }, + { url = "https://files.pythonhosted.org/packages/1c/12/734dce1087eed1875f2297f687e671cfe53a091b6f2f55f0c7241aad041b/numpy-2.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:87717eb24d4a8a64683b7a4e91ace04e2f5c7c77872f823f02a94feee186168f", size = 16628765, upload-time = "2025-06-07T14:45:35.076Z" }, + { url = "https://files.pythonhosted.org/packages/48/03/ffa41ade0e825cbcd5606a5669962419528212a16082763fc051a7247d76/numpy-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8fa264d56882b59dcb5ea4d6ab6f31d0c58a57b41aec605848b6eb2ef4a43e8", size = 15564335, upload-time = "2025-06-07T14:45:58.797Z" }, + { url = "https://files.pythonhosted.org/packages/07/58/869398a11863310aee0ff85a3e13b4c12f20d032b90c4b3ee93c3b728393/numpy-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e651756066a0eaf900916497e20e02fe1ae544187cb0fe88de981671ee7f6270", size = 18360608, upload-time = "2025-06-07T14:46:25.687Z" }, + { url = "https://files.pythonhosted.org/packages/2f/8a/5756935752ad278c17e8a061eb2127c9a3edf4ba2c31779548b336f23c8d/numpy-2.3.0-cp313-cp313-win32.whl", hash = "sha256:e43c3cce3b6ae5f94696669ff2a6eafd9a6b9332008bafa4117af70f4b88be6f", size = 6310005, upload-time = "2025-06-07T14:50:13.138Z" }, + { url = "https://files.pythonhosted.org/packages/08/60/61d60cf0dfc0bf15381eaef46366ebc0c1a787856d1db0c80b006092af84/numpy-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:81ae0bf2564cf475f94be4a27ef7bcf8af0c3e28da46770fc904da9abd5279b5", size = 12729093, upload-time = "2025-06-07T14:50:31.82Z" }, + { url = "https://files.pythonhosted.org/packages/66/31/2f2f2d2b3e3c32d5753d01437240feaa32220b73258c9eef2e42a0832866/numpy-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:c8738baa52505fa6e82778580b23f945e3578412554d937093eac9205e845e6e", size = 9885689, upload-time = "2025-06-07T14:50:47.888Z" }, + { url = "https://files.pythonhosted.org/packages/f1/89/c7828f23cc50f607ceb912774bb4cff225ccae7131c431398ad8400e2c98/numpy-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:39b27d8b38942a647f048b675f134dd5a567f95bfff481f9109ec308515c51d8", size = 20986612, upload-time = "2025-06-07T14:46:56.077Z" }, + { url = "https://files.pythonhosted.org/packages/dd/46/79ecf47da34c4c50eedec7511e53d57ffdfd31c742c00be7dc1d5ffdb917/numpy-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0eba4a1ea88f9a6f30f56fdafdeb8da3774349eacddab9581a21234b8535d3d3", size = 14298953, upload-time = "2025-06-07T14:47:18.053Z" }, + { url = "https://files.pythonhosted.org/packages/59/44/f6caf50713d6ff4480640bccb2a534ce1d8e6e0960c8f864947439f0ee95/numpy-2.3.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:b0f1f11d0a1da54927436505a5a7670b154eac27f5672afc389661013dfe3d4f", size = 5225806, upload-time = "2025-06-07T14:47:27.524Z" }, + { url = "https://files.pythonhosted.org/packages/a6/43/e1fd1aca7c97e234dd05e66de4ab7a5be54548257efcdd1bc33637e72102/numpy-2.3.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:690d0a5b60a47e1f9dcec7b77750a4854c0d690e9058b7bef3106e3ae9117808", size = 6735169, upload-time = "2025-06-07T14:47:38.057Z" }, + { url = "https://files.pythonhosted.org/packages/84/89/f76f93b06a03177c0faa7ca94d0856c4e5c4bcaf3c5f77640c9ed0303e1c/numpy-2.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:8b51ead2b258284458e570942137155978583e407babc22e3d0ed7af33ce06f8", size = 14330701, upload-time = "2025-06-07T14:47:59.113Z" }, + { url = "https://files.pythonhosted.org/packages/aa/f5/4858c3e9ff7a7d64561b20580cf7cc5d085794bd465a19604945d6501f6c/numpy-2.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:aaf81c7b82c73bd9b45e79cfb9476cb9c29e937494bfe9092c26aece812818ad", size = 16692983, upload-time = "2025-06-07T14:48:24.196Z" }, + { url = "https://files.pythonhosted.org/packages/08/17/0e3b4182e691a10e9483bcc62b4bb8693dbf9ea5dc9ba0b77a60435074bb/numpy-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f420033a20b4f6a2a11f585f93c843ac40686a7c3fa514060a97d9de93e5e72b", size = 15641435, upload-time = "2025-06-07T14:48:47.712Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d5/463279fda028d3c1efa74e7e8d507605ae87f33dbd0543cf4c4527c8b882/numpy-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d344ca32ab482bcf8735d8f95091ad081f97120546f3d250240868430ce52555", size = 18433798, upload-time = "2025-06-07T14:49:14.866Z" }, + { url = "https://files.pythonhosted.org/packages/0e/1e/7a9d98c886d4c39a2b4d3a7c026bffcf8fbcaf518782132d12a301cfc47a/numpy-2.3.0-cp313-cp313t-win32.whl", hash = "sha256:48a2e8eaf76364c32a1feaa60d6925eaf32ed7a040183b807e02674305beef61", size = 6438632, upload-time = "2025-06-07T14:49:25.67Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ab/66fc909931d5eb230107d016861824f335ae2c0533f422e654e5ff556784/numpy-2.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ba17f93a94e503551f154de210e4d50c5e3ee20f7e7a1b5f6ce3f22d419b93bb", size = 12868491, upload-time = "2025-06-07T14:49:44.898Z" }, + { url = "https://files.pythonhosted.org/packages/ee/e8/2c8a1c9e34d6f6d600c83d5ce5b71646c32a13f34ca5c518cc060639841c/numpy-2.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f14e016d9409680959691c109be98c436c6249eaf7f118b424679793607b5944", size = 9935345, upload-time = "2025-06-07T14:50:02.311Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a2/f8c1133f90eaa1c11bbbec1dc28a42054d0ce74bc2c9838c5437ba5d4980/numpy-2.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80b46117c7359de8167cc00a2c7d823bdd505e8c7727ae0871025a86d668283b", size = 21070759, upload-time = "2025-06-07T14:51:18.241Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e0/4c05fc44ba28463096eee5ae2a12832c8d2759cc5bcec34ae33386d3ff83/numpy-2.3.0-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:5814a0f43e70c061f47abd5857d120179609ddc32a613138cbb6c4e9e2dbdda5", size = 5301054, upload-time = "2025-06-07T14:51:27.413Z" }, + { url = "https://files.pythonhosted.org/packages/8a/3b/6c06cdebe922bbc2a466fe2105f50f661238ea223972a69c7deb823821e7/numpy-2.3.0-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:ef6c1e88fd6b81ac6d215ed71dc8cd027e54d4bf1d2682d362449097156267a2", size = 6817520, upload-time = "2025-06-07T14:51:38.015Z" }, + { url = "https://files.pythonhosted.org/packages/9d/a3/1e536797fd10eb3c5dbd2e376671667c9af19e241843548575267242ea02/numpy-2.3.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33a5a12a45bb82d9997e2c0b12adae97507ad7c347546190a18ff14c28bbca12", size = 14398078, upload-time = "2025-06-07T14:52:00.122Z" }, + { url = "https://files.pythonhosted.org/packages/7c/61/9d574b10d9368ecb1a0c923952aa593510a20df4940aa615b3a71337c8db/numpy-2.3.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:54dfc8681c1906d239e95ab1508d0a533c4a9505e52ee2d71a5472b04437ef97", size = 16751324, upload-time = "2025-06-07T14:52:25.077Z" }, + { url = "https://files.pythonhosted.org/packages/39/de/bcad52ce972dc26232629ca3a99721fd4b22c1d2bda84d5db6541913ef9c/numpy-2.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e017a8a251ff4d18d71f139e28bdc7c31edba7a507f72b1414ed902cbe48c74d", size = 12924237, upload-time = "2025-06-07T14:52:44.713Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[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 = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "phylib" +source = { editable = "." } +dependencies = [ + { name = "dask", version = "2024.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "dask", version = "2025.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "joblib" }, + { name = "mtscomp" }, + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "requests" }, + { name = "responses" }, + { name = "scipy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "toolz" }, + { name = "tqdm" }, +] + +[package.optional-dependencies] +dev = [ + { name = "coverage" }, + { name = "coveralls" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "responses" }, + { name = "ruff" }, +] +qt5 = [ + { name = "pyqt5" }, + { name = "pyqtwebengine" }, +] +qt6 = [ + { name = "pyqt6" }, + { name = "pyqt6-webengine" }, +] + +[package.dev-dependencies] +dev = [ + { name = "coverage" }, + { name = "coveralls" }, + { name = "memory-profiler" }, + { name = "mkdocs" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-qt" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "coverage", marker = "extra == 'dev'" }, + { name = "coveralls", marker = "extra == 'dev'" }, + { name = "dask" }, + { name = "joblib" }, + { name = "mtscomp" }, + { name = "numpy" }, + { name = "pyqt5", marker = "extra == 'qt5'", specifier = ">=5.12.0" }, + { name = "pyqt6", marker = "extra == 'qt6'", specifier = ">=6.9.1" }, + { name = "pyqt6-webengine", marker = "extra == 'qt6'", specifier = ">=6.9.0" }, + { name = "pyqtwebengine", marker = "extra == 'qt5'", specifier = ">=5.12.0" }, + { name = "pytest", marker = "extra == 'dev'" }, + { name = "pytest-cov", marker = "extra == 'dev'" }, + { name = "requests" }, + { name = "responses", specifier = ">=0.25.7" }, + { name = "responses", marker = "extra == 'dev'" }, + { name = "ruff", marker = "extra == 'dev'" }, + { name = "scipy" }, + { name = "toolz" }, + { name = "tqdm" }, +] +provides-extras = ["dev", "qt5", "qt6"] + +[package.metadata.requires-dev] +dev = [ + { name = "coverage", specifier = ">=6.0" }, + { name = "coveralls", specifier = ">=3.0" }, + { name = "memory-profiler", specifier = ">=0.60" }, + { name = "mkdocs", specifier = ">=1.4" }, + { name = "pytest", specifier = ">=6.0" }, + { name = "pytest-cov", specifier = ">=3.0" }, + { name = "pytest-qt", specifier = ">=4.0" }, + { name = "ruff", specifier = ">=0.1.0" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, +] + +[[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 = "psutil" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" }, + { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" }, + { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" }, + { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, +] + +[[package]] +name = "pyqt5" +version = "5.15.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyqt5-qt5" }, + { name = "pyqt5-sip" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/07/c9ed0bd428df6f87183fca565a79fee19fa7c88c7f00a7f011ab4379e77a/PyQt5-5.15.11.tar.gz", hash = "sha256:fda45743ebb4a27b4b1a51c6d8ef455c4c1b5d610c90d2934c7802b5c1557c52", size = 3216775, upload-time = "2024-07-19T08:39:57.756Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/64/42ec1b0bd72d87f87bde6ceb6869f444d91a2d601f2e67cd05febc0346a1/PyQt5-5.15.11-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:c8b03dd9380bb13c804f0bdb0f4956067f281785b5e12303d529f0462f9afdc2", size = 6579776, upload-time = "2024-07-19T08:39:19.775Z" }, + { url = "https://files.pythonhosted.org/packages/49/f5/3fb696f4683ea45d68b7e77302eff173493ac81e43d63adb60fa760b9f91/PyQt5-5.15.11-cp38-abi3-macosx_11_0_x86_64.whl", hash = "sha256:6cd75628f6e732b1ffcfe709ab833a0716c0445d7aec8046a48d5843352becb6", size = 7016415, upload-time = "2024-07-19T08:39:32.977Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8c/4065950f9d013c4b2e588fe33cf04e564c2322842d84dbcbce5ba1dc28b0/PyQt5-5.15.11-cp38-abi3-manylinux_2_17_x86_64.whl", hash = "sha256:cd672a6738d1ae33ef7d9efa8e6cb0a1525ecf53ec86da80a9e1b6ec38c8d0f1", size = 8188103, upload-time = "2024-07-19T08:39:40.561Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f0/ae5a5b4f9b826b29ea4be841b2f2d951bcf5ae1d802f3732b145b57c5355/PyQt5-5.15.11-cp38-abi3-win32.whl", hash = "sha256:76be0322ceda5deecd1708a8d628e698089a1cea80d1a49d242a6d579a40babd", size = 5433308, upload-time = "2024-07-19T08:39:46.932Z" }, + { url = "https://files.pythonhosted.org/packages/56/d5/68eb9f3d19ce65df01b6c7b7a577ad3bbc9ab3a5dd3491a4756e71838ec9/PyQt5-5.15.11-cp38-abi3-win_amd64.whl", hash = "sha256:bdde598a3bb95022131a5c9ea62e0a96bd6fb28932cc1619fd7ba211531b7517", size = 6865864, upload-time = "2024-07-19T08:39:53.572Z" }, +] + +[[package]] +name = "pyqt5-qt5" +version = "5.15.17" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/f9/accb06e76e23fb23053d48cc24fd78dec6ed14cb4d5cbadb0fd4a0c1b02e/PyQt5_Qt5-5.15.17-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:d8b8094108e748b4bbd315737cfed81291d2d228de43278f0b8bd7d2b808d2b9", size = 39972275, upload-time = "2025-05-24T11:15:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/87/1a/e1601ad6934cc489b8f1e967494f23958465cf1943712f054c5a306e9029/PyQt5_Qt5-5.15.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b68628f9b8261156f91d2f72ebc8dfb28697c4b83549245d9a68195bd2d74f0c", size = 37135109, upload-time = "2025-05-24T11:15:59.786Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e1/13d25a9ff2ac236a264b4603abaa39fa8bb9a7aa430519bb5f545c5b008d/PyQt5_Qt5-5.15.17-py3-none-manylinux2014_x86_64.whl", hash = "sha256:b018f75d1cc61146396fa5af14da1db77c5d6318030e5e366f09ffdf7bd358d8", size = 61112954, upload-time = "2025-05-24T11:16:26.036Z" }, +] + +[[package]] +name = "pyqt5-sip" +version = "12.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/79/086b50414bafa71df494398ad277d72e58229a3d1c1b1c766d12b14c2e6d/pyqt5_sip-12.17.0.tar.gz", hash = "sha256:682dadcdbd2239af9fdc0c0628e2776b820e128bec88b49b8d692fe682f90b4f", size = 104042, upload-time = "2025-02-02T17:13:11.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/23/1da570b7e143b6d216728c919cae2976f7dbff65db94e3d9f5b62df37ba5/PyQt5_sip-12.17.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec47914cc751608e587c1c2fdabeaf4af7fdc28b9f62796c583bea01c1a1aa3e", size = 122696, upload-time = "2025-02-02T17:12:35.786Z" }, + { url = "https://files.pythonhosted.org/packages/61/d5/506b1c3ad06268c601276572f1cde1c0dffd074b44e023f4d80f5ea49265/PyQt5_sip-12.17.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2f2a8dcc7626fe0da73a0918e05ce2460c7a14ddc946049310e6e35052105434", size = 270932, upload-time = "2025-02-02T17:12:38.175Z" }, + { url = "https://files.pythonhosted.org/packages/0b/9b/46159d8038374b076244a1930ead460e723453ec73f9b0330390ddfdd0ea/PyQt5_sip-12.17.0-cp310-cp310-win32.whl", hash = "sha256:0c75d28b8282be3c1d7dbc76950d6e6eba1e334783224e9b9835ce1a9c64f482", size = 49085, upload-time = "2025-02-02T17:12:40.146Z" }, + { url = "https://files.pythonhosted.org/packages/fe/66/b3eb937a620ce2a5db5c377beeca870d60fafd87aecc1bcca6921bbcf553/PyQt5_sip-12.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:8c4bc535bae0dfa764e8534e893619fe843ce5a2e25f901c439bcb960114f686", size = 59040, upload-time = "2025-02-02T17:12:41.962Z" }, + { url = "https://files.pythonhosted.org/packages/52/fd/7d6e3deca5ce37413956faf4e933ce6beb87ac0cc7b26d934b5ed998f88a/PyQt5_sip-12.17.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2c912807dd638644168ea8c7a447bfd9d85a19471b98c2c588c4d2e911c09b0a", size = 122748, upload-time = "2025-02-02T17:12:43.831Z" }, + { url = "https://files.pythonhosted.org/packages/29/4d/e5981cde03b091fd83a1ef4ef6a4ca99ce6921d61b80c0222fc8eafdc99a/PyQt5_sip-12.17.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:71514a7d43b44faa1d65a74ad2c5da92c03a251bdc749f009c313f06cceacc9a", size = 276401, upload-time = "2025-02-02T17:12:45.705Z" }, + { url = "https://files.pythonhosted.org/packages/5f/30/4c282896b1e8841639cf2aca59acf57d8b261ed834ae976c959f25fa4a35/PyQt5_sip-12.17.0-cp311-cp311-win32.whl", hash = "sha256:023466ae96f72fbb8419b44c3f97475de6642fa5632520d0f50fc1a52a3e8200", size = 49091, upload-time = "2025-02-02T17:12:47.688Z" }, + { url = "https://files.pythonhosted.org/packages/24/c1/50fc7301aa39a50f451fc1b6b219e778c540a823fe9533a57b4793c859fd/PyQt5_sip-12.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb565469d08dcb0a427def0c45e722323beb62db79454260482b6948bfd52d47", size = 59036, upload-time = "2025-02-02T17:12:49.535Z" }, + { url = "https://files.pythonhosted.org/packages/a3/e6/e51367c28d69b5a462f38987f6024e766fd8205f121fe2f4d8ba2a6886b9/PyQt5_sip-12.17.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ea08341c8a5da00c81df0d689ecd4ee47a95e1ecad9e362581c92513f2068005", size = 124650, upload-time = "2025-02-02T17:12:50.595Z" }, + { url = "https://files.pythonhosted.org/packages/64/3b/e6d1f772b41d8445d6faf86cc9da65910484ebd9f7df83abc5d4955437d0/PyQt5_sip-12.17.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4a92478d6808040fbe614bb61500fbb3f19f72714b99369ec28d26a7e3494115", size = 281893, upload-time = "2025-02-02T17:12:51.966Z" }, + { url = "https://files.pythonhosted.org/packages/ed/c5/d17fc2ddb9156a593710c88afd98abcf4055a2224b772f8bec2c6eea879c/PyQt5_sip-12.17.0-cp312-cp312-win32.whl", hash = "sha256:b0ff280b28813e9bfd3a4de99490739fc29b776dc48f1c849caca7239a10fc8b", size = 49438, upload-time = "2025-02-02T17:12:54.426Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c5/1174988d52c732d07033cf9a5067142b01d76be7731c6394a64d5c3ef65c/PyQt5_sip-12.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:54c31de7706d8a9a8c0fc3ea2c70468aba54b027d4974803f8eace9c22aad41c", size = 58017, upload-time = "2025-02-02T17:12:56.31Z" }, + { url = "https://files.pythonhosted.org/packages/fd/5d/f234e505af1a85189310521447ebc6052ebb697efded850d0f2b2555f7aa/PyQt5_sip-12.17.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c7a7ff355e369616b6bcb41d45b742327c104b2bf1674ec79b8d67f8f2fa9543", size = 124580, upload-time = "2025-02-02T17:12:58.158Z" }, + { url = "https://files.pythonhosted.org/packages/cd/cb/3b2050e9644d0021bdf25ddf7e4c3526e1edd0198879e76ba308e5d44faf/PyQt5_sip-12.17.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:419b9027e92b0b707632c370cfc6dc1f3b43c6313242fc4db57a537029bd179c", size = 281563, upload-time = "2025-02-02T17:12:59.421Z" }, + { url = "https://files.pythonhosted.org/packages/51/61/b8ebde7e0b32d0de44c521a0ace31439885b0423d7d45d010a2f7d92808c/PyQt5_sip-12.17.0-cp313-cp313-win32.whl", hash = "sha256:351beab964a19f5671b2a3e816ecf4d3543a99a7e0650f88a947fea251a7589f", size = 49383, upload-time = "2025-02-02T17:13:00.597Z" }, + { url = "https://files.pythonhosted.org/packages/15/ed/ff94d6b2910e7627380cb1fc9a518ff966e6d78285c8e54c9422b68305db/PyQt5_sip-12.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:672c209d05661fab8e17607c193bf43991d268a1eefbc2c4551fbf30fd8bb2ca", size = 58022, upload-time = "2025-02-02T17:13:01.738Z" }, + { url = "https://files.pythonhosted.org/packages/21/f7/3bed2754743ba52b8264c20a1c52df6ff9c5f6465c11ae108be3b841471a/PyQt5_sip-12.17.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d65a9c1b4cbbd8e856254609f56e897d2cb5c903f77b75fb720cb3a32c76b92b", size = 122688, upload-time = "2025-02-02T17:13:04.617Z" }, + { url = "https://files.pythonhosted.org/packages/23/63/8a934ea1f759eee60b4e0143e5b109d16560bf67593bfed76cca55799a1a/PyQt5_sip-12.17.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:32b03e7e77ecd7b4119eba486b0706fa59b490bcceb585f9b6ddec8a582082db", size = 268531, upload-time = "2025-02-02T17:13:06.005Z" }, + { url = "https://files.pythonhosted.org/packages/34/80/0df0cdb7b25a87346a493cedb20f6eeb0301e7fbc02ed23d9077df998291/PyQt5_sip-12.17.0-cp39-cp39-win32.whl", hash = "sha256:5b6c734f4ad28f3defac4890ed747d391d246af279200935d49953bc7d915b8c", size = 49005, upload-time = "2025-02-02T17:13:07.741Z" }, + { url = "https://files.pythonhosted.org/packages/30/f5/2fd274c4fe9513d750eecfbe0c39937a179534446e148d8b9db4255f429a/PyQt5_sip-12.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:855e8f5787d57e26a48d8c3de1220a8e92ab83be8d73966deac62fdae03ea2f9", size = 59076, upload-time = "2025-02-02T17:13:09.643Z" }, +] + +[[package]] +name = "pyqt6" +version = "6.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyqt6-qt6" }, + { name = "pyqt6-sip" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/1b/567f46eb43ca961efd38d7a0b73efb70d7342854f075fd919179fdb2a571/pyqt6-6.9.1.tar.gz", hash = "sha256:50642be03fb40f1c2111a09a1f5a0f79813e039c15e78267e6faaf8a96c1c3a6", size = 1067230, upload-time = "2025-06-06T08:49:30.307Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/c4/fc2a69cf3df09b213185ef5a677c3940cd20e7855d29e40061a685b9c6ee/pyqt6-6.9.1-cp39-abi3-macosx_10_14_universal2.whl", hash = "sha256:33c23d28f6608747ecc8bfd04c8795f61631af9db4fb1e6c2a7523ec4cc916d9", size = 59770566, upload-time = "2025-06-06T08:48:20.331Z" }, + { url = "https://files.pythonhosted.org/packages/d5/78/92f3c46440a83ebe22ae614bd6792e7b052bcb58ff128f677f5662015184/pyqt6-6.9.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:37884df27f774e2e1c0c96fa41e817a222329b80ffc6241725b0dc8c110acb35", size = 37804959, upload-time = "2025-06-06T08:48:39.587Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5e/e77fa2761d809cd08d724f44af01a4b6ceb0ff9648e43173187b0e4fac4e/pyqt6-6.9.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:055870b703c1a49ca621f8a89e2ec4d848e6c739d39367eb9687af3b056d9aa3", size = 40414608, upload-time = "2025-06-06T08:49:00.26Z" }, + { url = "https://files.pythonhosted.org/packages/c4/09/69cf80456b6a985e06dd24ed0c2d3451e43567bf2807a5f3a86ef7a74a2e/pyqt6-6.9.1-cp39-abi3-win_amd64.whl", hash = "sha256:15b95bd273bb6288b070ed7a9503d5ff377aa4882dd6d175f07cad28cdb21da0", size = 25717996, upload-time = "2025-06-06T08:49:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/52/b3/0839d8fd18b86362a4de384740f2f6b6885b5d06fda7720f8a335425e316/pyqt6-6.9.1-cp39-abi3-win_arm64.whl", hash = "sha256:08792c72d130a02e3248a120f0b9bbb4bf4319095f92865bc5b365b00518f53d", size = 25212132, upload-time = "2025-06-06T08:49:27.41Z" }, +] + +[[package]] +name = "pyqt6-qt6" +version = "6.9.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/40/04f652e714f85ba6b0c24f4ead860f2c5769f9e64737f415524d792d5914/pyqt6_qt6-6.9.1-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:3854c7f83ee4e8c2d91e23ab88b77f90e2ca7ace34fe72f634a446959f2b4d4a", size = 66236777, upload-time = "2025-06-03T14:53:17.684Z" }, + { url = "https://files.pythonhosted.org/packages/57/31/e4fa40568a59953ce5cf9a5adfbd1be4a806dafd94e39072d3cc0bed5468/pyqt6_qt6-6.9.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:123e4aeb037c099bb4696a3ea8edcb1d9d62cedd0b2b950556b26024c97f3293", size = 60551574, upload-time = "2025-06-03T14:53:48.42Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8d/7c8073cbbefe9c103ec8add70f29ffee1db95a3755b429b9f47cd6afa41b/pyqt6_qt6-6.9.1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:cc5bd193ebd2d1a3ec66e1eee65bf532d762c239459bce1ecebf56177243e89b", size = 82000130, upload-time = "2025-06-03T14:54:26.585Z" }, + { url = "https://files.pythonhosted.org/packages/1e/60/a4ab932028b0c15c0501cb52eb1e7f24f4ce2e4c78d46c7cce58a375a88c/pyqt6_qt6-6.9.1-py3-none-manylinux_2_39_aarch64.whl", hash = "sha256:b065af7243d1d450a49470a8185301196a18b1d41085d3ef476eb55bbb225083", size = 80463127, upload-time = "2025-06-03T14:55:03.272Z" }, + { url = "https://files.pythonhosted.org/packages/e7/85/552710819019a96d39d924071324a474aec54b31c410d7de8ebb398adcc1/pyqt6_qt6-6.9.1-py3-none-win_amd64.whl", hash = "sha256:f9e54c424bc921ecb76792a75d123e4ecfc26b00b0c57dae526f41f1d57951d3", size = 73778423, upload-time = "2025-06-03T14:55:39.756Z" }, + { url = "https://files.pythonhosted.org/packages/16/b4/70f6b18a4913f2326dcf7acb15c12cc0b91cb3932c2ba3b5728811f22acd/pyqt6_qt6-6.9.1-py3-none-win_arm64.whl", hash = "sha256:432caaedf5570bc8a9b7c75bc6af6a26bf88589536472eca73417ac019f59d41", size = 49617924, upload-time = "2025-06-03T14:57:13.038Z" }, +] + +[[package]] +name = "pyqt6-sip" +version = "13.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/4a/96daf6c2e4f689faae9bd8cebb52754e76522c58a6af9b5ec86a2e8ec8b4/pyqt6_sip-13.10.2.tar.gz", hash = "sha256:464ad156bf526500ce6bd05cac7a82280af6309974d816739b4a9a627156fafe", size = 92548, upload-time = "2025-05-23T12:26:49.901Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/a8/9eb019525f26801cf91ba38c8493ef641ee943d3b77885e78ac9fab11870/pyqt6_sip-13.10.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8132ec1cbbecc69d23dcff23916ec07218f1a9bbbc243bf6f1df967117ce303e", size = 110689, upload-time = "2025-05-23T12:26:21.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/29/79a2dba1cc6ec02c927dd0ffd596ca15ba0a2968123143bc00fc35f0173b/pyqt6_sip-13.10.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07f77e89d93747dda71b60c3490b00d754451729fbcbcec840e42084bf061655", size = 305804, upload-time = "2025-05-23T12:26:23.297Z" }, + { url = "https://files.pythonhosted.org/packages/bb/4f/fa8468f055679905d0e38d471ae16b5968896ee1d951477e162d9d0a712d/pyqt6_sip-13.10.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4ffa71ddff6ef031d52cd4f88b8bba08b3516313c023c7e5825cf4a0ba598712", size = 284059, upload-time = "2025-05-23T12:26:24.507Z" }, + { url = "https://files.pythonhosted.org/packages/e1/4e/abc995daaafe5ac55e00df0f42c4a5ee81473425a3250a20dc4301399842/pyqt6_sip-13.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:e907394795e61f1174134465c889177f584336a98d7a10beade2437bf5942244", size = 53410, upload-time = "2025-05-23T12:26:25.62Z" }, + { url = "https://files.pythonhosted.org/packages/75/9c/ea9ba7786f471ce025dff71653eec4a6c067d24d36d28cced457dd31314c/pyqt6_sip-13.10.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1a6c2f168773af9e6c7ef5e52907f16297d4efd346e4c958eda54ea9135be18e", size = 110707, upload-time = "2025-05-23T12:26:26.666Z" }, + { url = "https://files.pythonhosted.org/packages/d6/00/984a94f14ba378c802a8e304803bb6dc6961cd9f24befa1bf3987731f0c3/pyqt6_sip-13.10.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1d3cc9015a1bd8c8d3e86a009591e897d4d46b0c514aede7d2970a2208749cd", size = 317301, upload-time = "2025-05-23T12:26:28.182Z" }, + { url = "https://files.pythonhosted.org/packages/0d/b1/c3b433ebcee2503571d71be025de5dab4489d7153007fd5ae79c543eeedb/pyqt6_sip-13.10.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ddd578a8d975bfb5fef83751829bf09a97a1355fa1de098e4fb4d1b74ee872fc", size = 294277, upload-time = "2025-05-23T12:26:29.406Z" }, + { url = "https://files.pythonhosted.org/packages/24/96/4e909f0a4f7a9ad0076a0e200c10f96a5a09492efb683f3d66c885f9aba4/pyqt6_sip-13.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:061d4a2eb60a603d8be7db6c7f27eb29d9cea97a09aa4533edc1662091ce4f03", size = 53418, upload-time = "2025-05-23T12:26:30.536Z" }, + { url = "https://files.pythonhosted.org/packages/37/96/153c418d8c167fc56f2e62372b8862d577f3ece41b24c5205a05b0c2b0cd/pyqt6_sip-13.10.2-cp311-cp311-win_arm64.whl", hash = "sha256:45ac06f0380b7aa4fcffd89f9e8c00d1b575dc700c603446a9774fda2dcfc0de", size = 44969, upload-time = "2025-05-23T12:26:31.498Z" }, + { url = "https://files.pythonhosted.org/packages/22/5b/1240017e0d59575289ba52b58fd7f95e7ddf0ed2ede95f3f7e2dc845d337/pyqt6_sip-13.10.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:83e6a56d3e715f748557460600ec342cbd77af89ec89c4f2a68b185fa14ea46c", size = 112199, upload-time = "2025-05-23T12:26:32.503Z" }, + { url = "https://files.pythonhosted.org/packages/51/11/1fc3bae02a12a3ac8354aa579b56206286e8b5ca9586677b1058c81c2f74/pyqt6_sip-13.10.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ccf197f8fa410e076936bee28ad9abadb450931d5be5625446fd20e0d8b27a6", size = 322757, upload-time = "2025-05-23T12:26:33.752Z" }, + { url = "https://files.pythonhosted.org/packages/21/40/de9491213f480a27199690616959a17a0f234962b86aa1dd4ca2584e922d/pyqt6_sip-13.10.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:37af463dcce39285e686d49523d376994d8a2508b9acccb7616c4b117c9c4ed7", size = 304251, upload-time = "2025-05-23T12:26:35.66Z" }, + { url = "https://files.pythonhosted.org/packages/02/21/cc80e03f1052408c62c341e9fe9b81454c94184f4bd8a95d29d2ec86df92/pyqt6_sip-13.10.2-cp312-cp312-win_amd64.whl", hash = "sha256:c7b34a495b92790c70eae690d9e816b53d3b625b45eeed6ae2c0fe24075a237e", size = 53519, upload-time = "2025-05-23T12:26:36.797Z" }, + { url = "https://files.pythonhosted.org/packages/77/cf/53bd0863252b260a502659cb3124d9c9fe38047df9360e529b437b4ac890/pyqt6_sip-13.10.2-cp312-cp312-win_arm64.whl", hash = "sha256:c80cc059d772c632f5319632f183e7578cd0976b9498682833035b18a3483e92", size = 45349, upload-time = "2025-05-23T12:26:37.729Z" }, + { url = "https://files.pythonhosted.org/packages/a1/1e/979ea64c98ca26979d8ce11e9a36579e17d22a71f51d7366d6eec3c82c13/pyqt6_sip-13.10.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8b5d06a0eac36038fa8734657d99b5fe92263ae7a0cd0a67be6acfe220a063e1", size = 112227, upload-time = "2025-05-23T12:26:38.758Z" }, + { url = "https://files.pythonhosted.org/packages/d9/21/84c230048e3bfef4a9209d16e56dcd2ae10590d03a31556ae8b5f1dcc724/pyqt6_sip-13.10.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad376a6078da37b049fdf9d6637d71b52727e65c4496a80b753ddc8d27526aca", size = 322920, upload-time = "2025-05-23T12:26:39.856Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/c6a28a142f14e735088534cc92951c3f48cccd77cdd4f3b10d7996be420f/pyqt6_sip-13.10.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3dde8024d055f496eba7d44061c5a1ba4eb72fc95e5a9d7a0dbc908317e0888b", size = 303833, upload-time = "2025-05-23T12:26:41.075Z" }, + { url = "https://files.pythonhosted.org/packages/89/63/e5adf350c1c3123d4865c013f164c5265512fa79f09ad464fb2fdf9f9e61/pyqt6_sip-13.10.2-cp313-cp313-win_amd64.whl", hash = "sha256:0b097eb58b4df936c4a2a88a2f367c8bb5c20ff049a45a7917ad75d698e3b277", size = 53527, upload-time = "2025-05-23T12:26:42.625Z" }, + { url = "https://files.pythonhosted.org/packages/58/74/2df4195306d050fbf4963fb5636108a66e5afa6dc05fd9e81e51ec96c384/pyqt6_sip-13.10.2-cp313-cp313-win_arm64.whl", hash = "sha256:cc6a1dfdf324efaac6e7b890a608385205e652845c62130de919fd73a6326244", size = 45373, upload-time = "2025-05-23T12:26:43.536Z" }, + { url = "https://files.pythonhosted.org/packages/d1/39/4693dfad856ee9613fbf325916d980a76d5823f4da87fed76f00b48ee8ee/pyqt6_sip-13.10.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38b5823dca93377f8a4efac3cbfaa1d20229aa5b640c31cf6ebbe5c586333808", size = 110676, upload-time = "2025-05-23T12:26:44.593Z" }, + { url = "https://files.pythonhosted.org/packages/f0/42/6f7c2006871b20cf3e5073e3ffaa0bede0f8e2f8ccc2105c02e8d523c7d7/pyqt6_sip-13.10.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5506b9a795098df3b023cc7d0a37f93d3224a9c040c43804d4bc06e0b2b742b0", size = 303064, upload-time = "2025-05-23T12:26:46.19Z" }, + { url = "https://files.pythonhosted.org/packages/00/1c/38068f79d583fc9c2992553445634171e8b0bee6682be22cb8d4d18e7da6/pyqt6_sip-13.10.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e455a181d45a28ee8d18d42243d4f470d269e6ccdee60f2546e6e71218e05bb4", size = 281774, upload-time = "2025-05-23T12:26:47.413Z" }, + { url = "https://files.pythonhosted.org/packages/aa/97/70cad9a770a56a2efb30c120fb1619ed81a6058c014cdcda5133429ad033/pyqt6_sip-13.10.2-cp39-cp39-win_amd64.whl", hash = "sha256:9c67ed66e21b11e04ffabe0d93bc21df22e0a5d7e2e10ebc8c1d77d2f5042991", size = 53630, upload-time = "2025-05-23T12:26:48.534Z" }, +] + +[[package]] +name = "pyqt6-webengine" +version = "6.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyqt6" }, + { name = "pyqt6-sip" }, + { name = "pyqt6-webengine-qt6" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/1a/9971af004a7e859347702f816fb71ecd67c3e32b2f0ae8daf1c1ded99f62/pyqt6_webengine-6.9.0.tar.gz", hash = "sha256:6ae537e3bbda06b8e06535e4852297e0bc3b00543c47929541fcc9b11981aa25", size = 34616, upload-time = "2025-04-08T08:57:35.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/e1/964ee1c464a0e1f07f8be54ce9316dc76e431d1bc99c9e5c1437bf548d92/PyQt6_WebEngine-6.9.0-cp39-abi3-macosx_10_14_universal2.whl", hash = "sha256:3ea5bdd48d109f35bf726f59d85b250e430ddd50175fe79a386b7f14d3e34d2d", size = 438004, upload-time = "2025-04-08T08:57:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9d/8674bb27e2497fdad7ae5bc000831b42dbfb546aacd11ae7a8cca4493190/PyQt6_WebEngine-6.9.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c15012245036604c82abcd865e0b808e75bcfd0b477454298b7a70d9e6c4958b", size = 299003, upload-time = "2025-04-08T08:57:31.334Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9c/b6a1ce7026260d518a103c467a4795c719dd1e4d7f8dc00416d3ec292d3a/PyQt6_WebEngine-6.9.0-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:e4404899290f86d4652a07471262a2f41397c64ecb091229b5bbbd8b82af35ce", size = 297424, upload-time = "2025-04-08T08:57:32.664Z" }, + { url = "https://files.pythonhosted.org/packages/0e/e8/444487c86472c522d6ab28686b9f3c4d6fe2febde81b40561d42c11b5cd7/PyQt6_WebEngine-6.9.0-cp39-abi3-win_amd64.whl", hash = "sha256:541cf838facadfc38243baaecfeeaf07c8eff030cf27341c85c245d00e571489", size = 237847, upload-time = "2025-04-08T08:57:34.174Z" }, +] + +[[package]] +name = "pyqt6-webengine-qt6" +version = "6.9.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/68/8eccda2a78d100a95db2131fac82b4ad842bdf0255019dcf86d5db0db3fa/pyqt6_webengine_qt6-6.9.1-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:38a461e21df3e09829ce18cd0ecd052ecff0f9a4001ae000ea6ddac10fae6b0f", size = 120033076, upload-time = "2025-06-03T14:59:49.52Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8a/a2cf83dd9cb8e944507e7b4070ac537a30d6caef7e498c87f1612507431d/pyqt6_webengine_qt6-6.9.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9351ac6cccfdbf414a2514b58d49765d3f48fb3b690ceeaa8ca804340eb460b4", size = 108530410, upload-time = "2025-06-03T15:00:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/fb/82/99b189e30641469dda6eb09a5f3cf0c5e12e2b39967e1df5d156f8499c4f/pyqt6_webengine_qt6-6.9.1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:37f9d33e3f68681687a7d28b80058c6473813c6c2f9706a44dc7bc07486b9e9a", size = 110750602, upload-time = "2025-06-03T15:01:32.055Z" }, + { url = "https://files.pythonhosted.org/packages/68/fc/958c7f5e0f9cfe7903a90ed41b435b17de6ca7cf5d1f73e97ad07fe8107c/pyqt6_webengine_qt6-6.9.1-py3-none-manylinux_2_39_aarch64.whl", hash = "sha256:e8440e65b79df167b49bd3c26b8bf7179306df010e8c319a7ad6b8bc2a8ae8d4", size = 106652228, upload-time = "2025-06-03T15:02:18.985Z" }, + { url = "https://files.pythonhosted.org/packages/4c/82/4a3e2233ca3aa9c3ec77390e570fbcffcc511bc8513461daa1d38cda652a/pyqt6_webengine_qt6-6.9.1-py3-none-win_amd64.whl", hash = "sha256:c7f460226c054a52b7868d3befeed4fe2af03f4f80d2e53ab49fcb238bac2bc7", size = 110244976, upload-time = "2025-06-03T15:03:09.743Z" }, +] + +[[package]] +name = "pyqtwebengine" +version = "5.15.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyqt5" }, + { name = "pyqt5-sip" }, + { name = "pyqtwebengine-qt5" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/e8/19a00646866e950307f8cd73841575cdb92800ae14837d5821bcbb91392c/PyQtWebEngine-5.15.7.tar.gz", hash = "sha256:f121ac6e4a2f96ac289619bcfc37f64e68362f24a346553f5d6c42efa4228a4d", size = 32223, upload-time = "2024-07-19T08:44:48.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/3d/8961b3bb00c0979280a1a160c745e1d543b4d5823f8a71dfa370898b5699/PyQtWebEngine-5.15.7-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:e17187d9a3db3bab041f31385ed72832312557fefc5bd63ae4692df306dc1572", size = 181029, upload-time = "2024-07-19T08:44:31.266Z" }, + { url = "https://files.pythonhosted.org/packages/8e/c7/82bdc50b44f505a87e6a9d7f4a4d017c8e2f06b9f3ab8f661adff00b95c6/PyQtWebEngine-5.15.7-cp38-abi3-macosx_11_0_x86_64.whl", hash = "sha256:021814af1ff7d8be447c5314891cd4ddc931deae393dc2d38a816569aa0eb8cd", size = 187313, upload-time = "2024-07-19T08:44:43.002Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a0/b36e7d6f0cd69b7dd58f0d733188e100115c5fce1c0606ad84bf35ef7ceb/PyQtWebEngine-5.15.7-cp38-abi3-manylinux_2_17_x86_64.whl", hash = "sha256:965461ca0cf414e03bd510a9a0e2ea36dc21deaa7fc4a631be4a1f2aa0327179", size = 227640, upload-time = "2024-07-19T08:44:44.236Z" }, + { url = "https://files.pythonhosted.org/packages/54/b9/0e68e30cec6a02d8d27c7663de77460156c5342848e2f72424e577c66eaf/PyQtWebEngine-5.15.7-cp38-abi3-win32.whl", hash = "sha256:c0680527b1af3e0145ce5e0f2ba2156ff0b4b38844392cf0ddd37ede6a9edeab", size = 160980, upload-time = "2024-07-19T08:44:45.482Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/0dead50889d905fc99f40e61e5ab7f73746605ce8f74c4fa7fb3fc1d6c5e/PyQtWebEngine-5.15.7-cp38-abi3-win_amd64.whl", hash = "sha256:bd5e8c426d6f6b352cd15800d64a89b2a4a11e098460b818c7bdcf5e5612e44f", size = 184657, upload-time = "2024-07-19T08:44:47.066Z" }, +] + +[[package]] +name = "pyqtwebengine-qt5" +version = "5.15.17" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/b9/ef6286ba4f3bb12d12addb3809808f6b2c6491b330286076c9a51b59363d/PyQtWebEngine_Qt5-5.15.17-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:f46be013bdfa883c328c9fff554c6443671833a2ef5b7f649bd33b0d5cf09d2f", size = 74866238, upload-time = "2025-05-24T11:17:22.441Z" }, + { url = "https://files.pythonhosted.org/packages/55/66/515bbd2e15930b11b5d0e237cd6cca5210c72bff3da9a847affbc0ea3a73/PyQtWebEngine_Qt5-5.15.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:508b8e5083b5fc88c93ac22d157198576931ce3cacbdec7ef86687c0f957e87a", size = 67346127, upload-time = "2025-05-24T11:17:52.092Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5f/a611480315a4a7cc369d567b91eab776e6170003317f595d24a8432e7b68/PyQtWebEngine_Qt5-5.15.17-py3-none-manylinux2014_x86_64.whl", hash = "sha256:daee5f95692f5dccd0c34423bce036d31dce44e02848e9ed6923fd669ac02a7a", size = 90628242, upload-time = "2025-05-24T11:18:33.302Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/aa/405082ce2749be5398045152251ac69c0f3578c7077efc53431303af97ce/pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6", size = 1515232, upload-time = "2025-06-02T17:36:30.03Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/de/afa024cbe022b1b318a3d224125aa24939e99b4ff6f22e0ba639a2eaee47/pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e", size = 363797, upload-time = "2025-06-02T17:36:27.859Z" }, +] + +[[package]] +name = "pytest-cov" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857, upload-time = "2025-04-05T14:07:51.592Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841, upload-time = "2025-04-05T14:07:49.641Z" }, +] + +[[package]] +name = "pytest-qt" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/2c/6a477108342bbe1f5a81a2c54c86c3efadc35f6ad47c76f00c75764a0f7c/pytest-qt-4.4.0.tar.gz", hash = "sha256:76896142a940a4285339008d6928a36d4be74afec7e634577e842c9cc5c56844", size = 125443, upload-time = "2024-02-07T21:22:15.849Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/51/6cc5b9c1ecdcd78e6cde97e03d05f5a4ace8f720c5ce0f26f9dce474a0da/pytest_qt-4.4.0-py3-none-any.whl", hash = "sha256:001ed2f8641764b394cf286dc8a4203e40eaf9fff75bf0bfe5103f7f8d0c591d", size = 36286, upload-time = "2024-02-07T21:22:13.295Z" }, +] + +[[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 = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, + { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777, upload-time = "2024-08-06T20:33:25.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318, upload-time = "2024-08-06T20:33:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891, upload-time = "2024-08-06T20:33:28.974Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614, upload-time = "2024-08-06T20:33:34.157Z" }, + { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360, upload-time = "2024-08-06T20:33:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006, upload-time = "2024-08-06T20:33:37.501Z" }, + { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577, upload-time = "2024-08-06T20:33:39.389Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593, upload-time = "2024-08-06T20:33:46.63Z" }, + { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312, upload-time = "2024-08-06T20:33:49.073Z" }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, +] + +[[package]] +name = "responses" +version = "0.25.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/7e/2345ac3299bd62bd7163216702bbc88976c099cfceba5b889f2a457727a1/responses-0.25.7.tar.gz", hash = "sha256:8ebae11405d7a5df79ab6fd54277f6f2bc29b2d002d0dd2d5c632594d1ddcedb", size = 79203, upload-time = "2025-03-11T15:36:16.624Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/fc/1d20b64fa90e81e4fa0a34c9b0240a6cfb1326b7e06d18a5432a9917c316/responses-0.25.7-py3-none-any.whl", hash = "sha256:92ca17416c90fe6b35921f52179bff29332076bb32694c0df02dcac2c6bc043c", size = 34732, upload-time = "2025-03-11T15:36:14.589Z" }, +] + +[[package]] +name = "ruff" +version = "0.11.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/da/9c6f995903b4d9474b39da91d2d626659af3ff1eeb43e9ae7c119349dba6/ruff-0.11.13.tar.gz", hash = "sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514", size = 4282054, upload-time = "2025-06-05T21:00:15.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/ce/a11d381192966e0b4290842cc8d4fac7dc9214ddf627c11c1afff87da29b/ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46", size = 10292516, upload-time = "2025-06-05T20:59:32.944Z" }, + { url = "https://files.pythonhosted.org/packages/78/db/87c3b59b0d4e753e40b6a3b4a2642dfd1dcaefbff121ddc64d6c8b47ba00/ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48", size = 11106083, upload-time = "2025-06-05T20:59:37.03Z" }, + { url = "https://files.pythonhosted.org/packages/77/79/d8cec175856ff810a19825d09ce700265f905c643c69f45d2b737e4a470a/ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b", size = 10436024, upload-time = "2025-06-05T20:59:39.741Z" }, + { url = "https://files.pythonhosted.org/packages/8b/5b/f6d94f2980fa1ee854b41568368a2e1252681b9238ab2895e133d303538f/ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a", size = 10646324, upload-time = "2025-06-05T20:59:42.185Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9c/b4c2acf24ea4426016d511dfdc787f4ce1ceb835f3c5fbdbcb32b1c63bda/ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc", size = 10174416, upload-time = "2025-06-05T20:59:44.319Z" }, + { url = "https://files.pythonhosted.org/packages/f3/10/e2e62f77c65ede8cd032c2ca39c41f48feabedb6e282bfd6073d81bb671d/ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629", size = 11724197, upload-time = "2025-06-05T20:59:46.935Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f0/466fe8469b85c561e081d798c45f8a1d21e0b4a5ef795a1d7f1a9a9ec182/ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933", size = 12511615, upload-time = "2025-06-05T20:59:49.534Z" }, + { url = "https://files.pythonhosted.org/packages/17/0e/cefe778b46dbd0cbcb03a839946c8f80a06f7968eb298aa4d1a4293f3448/ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165", size = 12117080, upload-time = "2025-06-05T20:59:51.654Z" }, + { url = "https://files.pythonhosted.org/packages/5d/2c/caaeda564cbe103bed145ea557cb86795b18651b0f6b3ff6a10e84e5a33f/ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71", size = 11326315, upload-time = "2025-06-05T20:59:54.469Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/782e7d681d660eda8c536962920c41309e6dd4ebcea9a2714ed5127d44bd/ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9", size = 11555640, upload-time = "2025-06-05T20:59:56.986Z" }, + { url = "https://files.pythonhosted.org/packages/5d/d4/3d580c616316c7f07fb3c99dbecfe01fbaea7b6fd9a82b801e72e5de742a/ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc", size = 10507364, upload-time = "2025-06-05T20:59:59.154Z" }, + { url = "https://files.pythonhosted.org/packages/5a/dc/195e6f17d7b3ea6b12dc4f3e9de575db7983db187c378d44606e5d503319/ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7", size = 10141462, upload-time = "2025-06-05T21:00:01.481Z" }, + { url = "https://files.pythonhosted.org/packages/f4/8e/39a094af6967faa57ecdeacb91bedfb232474ff8c3d20f16a5514e6b3534/ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432", size = 11121028, upload-time = "2025-06-05T21:00:04.06Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c0/b0b508193b0e8a1654ec683ebab18d309861f8bd64e3a2f9648b80d392cb/ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492", size = 11602992, upload-time = "2025-06-05T21:00:06.249Z" }, + { url = "https://files.pythonhosted.org/packages/7c/91/263e33ab93ab09ca06ce4f8f8547a858cc198072f873ebc9be7466790bae/ruff-0.11.13-py3-none-win32.whl", hash = "sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250", size = 10474944, upload-time = "2025-06-05T21:00:08.459Z" }, + { url = "https://files.pythonhosted.org/packages/46/f4/7c27734ac2073aae8efb0119cae6931b6fb48017adf048fdf85c19337afc/ruff-0.11.13-py3-none-win_amd64.whl", hash = "sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3", size = 11548669, upload-time = "2025-06-05T21:00:11.147Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bf/b273dd11673fed8a6bd46032c0ea2a04b2ac9bfa9c628756a5856ba113b0/ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b", size = 10683928, upload-time = "2025-06-05T21:00:13.758Z" }, +] + +[[package]] +name = "scipy" +version = "1.13.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/00/48c2f661e2816ccf2ecd77982f6605b2950afe60f60a52b4cbbc2504aa8f/scipy-1.13.1.tar.gz", hash = "sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c", size = 57210720, upload-time = "2024-05-23T03:29:26.079Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/59/41b2529908c002ade869623b87eecff3e11e3ce62e996d0bdcb536984187/scipy-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca", size = 39328076, upload-time = "2024-05-23T03:19:01.687Z" }, + { url = "https://files.pythonhosted.org/packages/d5/33/f1307601f492f764062ce7dd471a14750f3360e33cd0f8c614dae208492c/scipy-1.13.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f", size = 30306232, upload-time = "2024-05-23T03:19:09.089Z" }, + { url = "https://files.pythonhosted.org/packages/c0/66/9cd4f501dd5ea03e4a4572ecd874936d0da296bd04d1c45ae1a4a75d9c3a/scipy-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989", size = 33743202, upload-time = "2024-05-23T03:19:15.138Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ba/7255e5dc82a65adbe83771c72f384d99c43063648456796436c9a5585ec3/scipy-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f", size = 38577335, upload-time = "2024-05-23T03:19:21.984Z" }, + { url = "https://files.pythonhosted.org/packages/49/a5/bb9ded8326e9f0cdfdc412eeda1054b914dfea952bda2097d174f8832cc0/scipy-1.13.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94", size = 38820728, upload-time = "2024-05-23T03:19:28.225Z" }, + { url = "https://files.pythonhosted.org/packages/12/30/df7a8fcc08f9b4a83f5f27cfaaa7d43f9a2d2ad0b6562cced433e5b04e31/scipy-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54", size = 46210588, upload-time = "2024-05-23T03:19:35.661Z" }, + { url = "https://files.pythonhosted.org/packages/b4/15/4a4bb1b15bbd2cd2786c4f46e76b871b28799b67891f23f455323a0cdcfb/scipy-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9", size = 39333805, upload-time = "2024-05-23T03:19:43.081Z" }, + { url = "https://files.pythonhosted.org/packages/ba/92/42476de1af309c27710004f5cdebc27bec62c204db42e05b23a302cb0c9a/scipy-1.13.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326", size = 30317687, upload-time = "2024-05-23T03:19:48.799Z" }, + { url = "https://files.pythonhosted.org/packages/80/ba/8be64fe225360a4beb6840f3cbee494c107c0887f33350d0a47d55400b01/scipy-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299", size = 33694638, upload-time = "2024-05-23T03:19:55.104Z" }, + { url = "https://files.pythonhosted.org/packages/36/07/035d22ff9795129c5a847c64cb43c1fa9188826b59344fee28a3ab02e283/scipy-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa", size = 38569931, upload-time = "2024-05-23T03:20:01.82Z" }, + { url = "https://files.pythonhosted.org/packages/d9/10/f9b43de37e5ed91facc0cfff31d45ed0104f359e4f9a68416cbf4e790241/scipy-1.13.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59", size = 38838145, upload-time = "2024-05-23T03:20:09.173Z" }, + { url = "https://files.pythonhosted.org/packages/4a/48/4513a1a5623a23e95f94abd675ed91cfb19989c58e9f6f7d03990f6caf3d/scipy-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b", size = 46196227, upload-time = "2024-05-23T03:20:16.433Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7b/fb6b46fbee30fc7051913068758414f2721003a89dd9a707ad49174e3843/scipy-1.13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1", size = 39357301, upload-time = "2024-05-23T03:20:23.538Z" }, + { url = "https://files.pythonhosted.org/packages/dc/5a/2043a3bde1443d94014aaa41e0b50c39d046dda8360abd3b2a1d3f79907d/scipy-1.13.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d", size = 30363348, upload-time = "2024-05-23T03:20:29.885Z" }, + { url = "https://files.pythonhosted.org/packages/e7/cb/26e4a47364bbfdb3b7fb3363be6d8a1c543bcd70a7753ab397350f5f189a/scipy-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627", size = 33406062, upload-time = "2024-05-23T03:20:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/88/ab/6ecdc526d509d33814835447bbbeedbebdec7cca46ef495a61b00a35b4bf/scipy-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884", size = 38218311, upload-time = "2024-05-23T03:20:42.086Z" }, + { url = "https://files.pythonhosted.org/packages/0b/00/9f54554f0f8318100a71515122d8f4f503b1a2c4b4cfab3b4b68c0eb08fa/scipy-1.13.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16", size = 38442493, upload-time = "2024-05-23T03:20:48.292Z" }, + { url = "https://files.pythonhosted.org/packages/3e/df/963384e90733e08eac978cd103c34df181d1fec424de383cdc443f418dd4/scipy-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949", size = 45910955, upload-time = "2024-05-23T03:20:55.091Z" }, + { url = "https://files.pythonhosted.org/packages/7f/29/c2ea58c9731b9ecb30b6738113a95d147e83922986b34c685b8f6eefde21/scipy-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5", size = 39352927, upload-time = "2024-05-23T03:21:01.95Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c0/e71b94b20ccf9effb38d7147c0064c08c622309fd487b1b677771a97d18c/scipy-1.13.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24", size = 30324538, upload-time = "2024-05-23T03:21:07.634Z" }, + { url = "https://files.pythonhosted.org/packages/6d/0f/aaa55b06d474817cea311e7b10aab2ea1fd5d43bc6a2861ccc9caec9f418/scipy-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004", size = 33732190, upload-time = "2024-05-23T03:21:14.41Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/d0ad1a96f80962ba65e2ce1de6a1e59edecd1f0a7b55990ed208848012e0/scipy-1.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637e98dcf185ba7f8e663e122ebf908c4702420477ae52a04f9908707456ba4d", size = 38612244, upload-time = "2024-05-23T03:21:21.827Z" }, + { url = "https://files.pythonhosted.org/packages/8d/02/1165905f14962174e6569076bcc3315809ae1291ed14de6448cc151eedfd/scipy-1.13.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c", size = 38845637, upload-time = "2024-05-23T03:21:28.729Z" }, + { url = "https://files.pythonhosted.org/packages/3e/77/dab54fe647a08ee4253963bcd8f9cf17509c8ca64d6335141422fe2e2114/scipy-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2", size = 46227440, upload-time = "2024-05-23T03:21:35.888Z" }, +] + +[[package]] +name = "scipy" +version = "1.15.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" }, + { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" }, + { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, + { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, + { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, + { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, + { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, + { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, + { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, + { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" }, + { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" }, + { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" }, + { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" }, + { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" }, + { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" }, + { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" }, + { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, +] + +[[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 = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, +] + +[[package]] +name = "toolz" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/0b/d80dfa675bf592f636d1ea0b835eab4ec8df6e9415d8cfd766df54456123/toolz-1.0.0.tar.gz", hash = "sha256:2c86e3d9a04798ac556793bced838816296a2f085017664e4995cb40a1047a02", size = 66790, upload-time = "2024-10-04T16:17:04.001Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/98/eb27cc78ad3af8e302c9d8ff4977f5026676e130d28dd7578132a457170c/toolz-1.0.0-py3-none-any.whl", hash = "sha256:292c8f1c4e7516bf9086f8850935c799a874039c8bcf959d47b600e4c44a6236", size = 56383, upload-time = "2024-10-04T16:17:01.533Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, +] + +[[package]] +name = "urllib3" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" }, +] + +[[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/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, + { 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/05/52/7223011bb760fce8ddc53416beb65b83a3ea6d7d13738dde75eeb2c89679/watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8", size = 96390, upload-time = "2024-11-01T14:06:49.325Z" }, + { url = "https://files.pythonhosted.org/packages/9c/62/d2b21bc4e706d3a9d467561f487c2938cbd881c69f3808c43ac1ec242391/watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a", size = 88386, upload-time = "2024-11-01T14:06:50.536Z" }, + { url = "https://files.pythonhosted.org/packages/ea/22/1c90b20eda9f4132e4603a26296108728a8bfe9584b006bd05dd94548853/watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c", size = 89017, upload-time = "2024-11-01T14:06:51.717Z" }, + { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, + { url = "https://files.pythonhosted.org/packages/5b/79/69f2b0e8d3f2afd462029031baafb1b75d11bb62703f0e1022b2e54d49ee/watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa", size = 87903, upload-time = "2024-11-01T14:06:57.052Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2b/dc048dd71c2e5f0f7ebc04dd7912981ec45793a03c0dc462438e0591ba5d/watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e", size = 88381, upload-time = "2024-11-01T14:06:58.193Z" }, + { 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 = "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" }, +]