diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..11735f533 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,115 @@ +name: CI + +on: + push: + branches: [ main, master, dev ] + pull_request: + branches: [ main, master, dev ] + +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"] + qt-version: ["qt5", "qt6"] + exclude: + # PyQt6 might have issues on some older Python versions + - python-version: "3.9" + qt-version: "qt6" + + steps: + - uses: actions/checkout@v4 + + # Set up display for GUI testing on Linux + - name: Set up display (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y xvfb herbstluftwm + export DISPLAY=:99.0 + Xvfb :99 -screen 0 1400x900x24 -ac +extension GLX +render & + sleep 3 + herbstluftwm & + sleep 1 + env: + DISPLAY: :99.0 + + - 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 --extra ${{ matrix.qt-version }} + + - name: Install phylib from branch + run: | + if [ "${{ github.ref_name }}" = "master" ]; then + uv add "git+https://github.com/cortex-lab/phylib.git@master" + elif [ "${{ github.ref_name }}" = "dev" ]; then + uv add "git+https://github.com/cortex-lab/phylib.git@dev" + else + uv add "git+https://github.com/cortex-lab/phylib.git@${{ github.ref_name }}" + fi + shell: bash + + - name: Install additional test dependencies + run: | + uv add "git+https://github.com/kwikteam/klusta.git" + uv add "git+https://github.com/kwikteam/klustakwik2.git" + + - name: Lint with ruff + run: uv run ruff check phy + + - name: Check formatting with ruff + run: uv run ruff format --check phy + + - name: Test with pytest (Linux) + if: runner.os == 'Linux' + run: uv run make test-full + env: + DISPLAY: :99.0 + QT_QPA_PLATFORM: offscreen + + - name: Test with pytest (Windows/macOS) + if: runner.os != 'Linux' + run: uv run pytest --cov=phy --cov-report=xml phy + env: + QT_QPA_PLATFORM: offscreen + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + file: ./coverage.xml + fail_ci_if_error: false + 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/.gitignore b/.gitignore index 859487e4a..0330682f1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +local_tests contrib data doc diff --git a/Makefile b/Makefile index e095c074a..3fe0f9cbf 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,29 +10,66 @@ 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 --extra qt5 + +install-qt6: + uv sync --dev --extra qt6 lint: - flake8 phy + uv run ruff check phy + +format: + uv run ruff format phy + +format-check: + uv run ruff format --check phy + +lint-fix: + uv run ruff check --fix phy -# Test everything except apps. -test: lint - py.test -xvv --cov-report= --cov=phy phy --ignore=phy/apps/ --cov-append - coverage report --omit */phy/apps/*,*/phy/plot/gloo/* +# Test everything except apps +test: lint format-check + uv run pytest -xvv --cov-report= --cov=phy phy --ignore=phy/apps/ --cov-append + uv run coverage report --omit "*/phy/apps/*,*/phy/plot/gloo/*" -# Test just the apps. +# Test just the apps test-apps: lint - py.test --cov-report term-missing --cov=phy.apps phy/apps/ --cov-append + uv run pytest --cov-report term-missing --cov=phy.apps phy/apps/ --cov-append -# Test everything. +# Test everything test-full: test test-apps - coverage report --omit */phy/plot/gloo/* + uv run coverage report --omit "*/phy/plot/gloo/*" + +test-fast: + uv run pytest phy doc: - python3 tools/api.py && python tools/extract_shortcuts.py && python tools/plugins_doc.py + uv run python tools/api.py && uv run python tools/extract_shortcuts.py && uv run python tools/plugins_doc.py build: - python3 setup.py sdist --formats=zip + uv build upload: - twine upload dist/* + uv publish + +upload-test: + uv publish --publish-url https://test.pypi.org/legacy/ + +coverage: + uv run coverage html + +dev: install lint format test + +ci: lint format-check test-full build + +.PHONY: clean-build clean-pyc clean-test clean install install-qt6 lint format format-check lint-fix test test-apps test-full test-fast doc build upload upload-test coverage 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 65% rename from setup.py rename to deprecated/setup.py index e65eb1ff2..877e58b55 100644 --- a/setup.py +++ b/deprecated/setup.py @@ -4,9 +4,9 @@ """Installation script.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import os import os.path as op @@ -15,15 +15,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 @@ -52,23 +55,34 @@ def _package_tree(pkgroot): setup( name='phy', version=version, - license="BSD", + license='BSD', description='Interactive visualization and manual spike sorting of large-scale ephys data', long_description=readme, - long_description_content_type="text/markdown", + long_description_content_type='text/markdown', author='Cyrille Rossant (cortex-lab/UCL/IBL)', author_email='cyrille.rossant+pypi@gmail.com', url='https://phy.cortexlab.net', packages=_package_tree('phy'), package_dir={'phy': 'phy'}, package_data={ - 'phy': ['*.vert', '*.frag', '*.glsl', '*.npy', '*.gz', '*.txt', '*.json', - '*.html', '*.css', '*.js', '*.prb', '*.ttf', '*.png'], + 'phy': [ + '*.vert', + '*.frag', + '*.glsl', + '*.npy', + '*.gz', + '*.txt', + '*.json', + '*.html', + '*.css', + '*.js', + '*.prb', + '*.ttf', + '*.png', + ], }, entry_points={ - 'console_scripts': [ - 'phy = phy.apps:phycli' - ], + 'console_scripts': ['phy = phy.apps:phycli'], }, install_requires=require, include_package_data=True, @@ -78,7 +92,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/phy/__init__.py b/phy/__init__.py index b1e036f4c..4e90465e5 100644 --- a/phy/__init__.py +++ b/phy/__init__.py @@ -1,12 +1,11 @@ -# -*- coding: utf-8 -*- # flake8: noqa """phy: interactive visualization and manual spike sorting of large-scale ephys data.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import atexit import logging @@ -22,9 +21,9 @@ from .utils.plugin import IPlugin, get_plugin, discover_plugins -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Global variables and functions -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ __author__ = 'Cyrille Rossant' __email__ = 'cyrille.rossant at gmail.com' @@ -50,4 +49,5 @@ def on_exit(): # pragma: no cover def test(): # pragma: no cover """Run the full testing suite of phy.""" import pytest + pytest.main() diff --git a/phy/apps/__init__.py b/phy/apps/__init__.py index de67a26fb..db51602ea 100644 --- a/phy/apps/__init__.py +++ b/phy/apps/__init__.py @@ -1,37 +1,31 @@ -# -*- coding: utf-8 -*- - """CLI tool.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ -from contextlib import contextmanager import logging -from pathlib import Path import sys +from contextlib import contextmanager +from pathlib import Path from traceback import format_exception import click - -from phylib import add_default_handler, _Formatter # noqa -from phylib import _logger_date_fmt, _logger_fmt # noqa +from phylib import _Formatter, _logger_date_fmt, _logger_fmt, add_default_handler # noqa # noqa from phy import __version_git__ from phy.gui.qt import QtDialogLogger -from phy.utils.profiling import _enable_profiler, _enable_pdb - -from .base import ( # noqa - BaseController, WaveformMixin, FeatureMixin, TemplateMixin, TraceMixin) +from phy.utils.profiling import _enable_pdb, _enable_profiler +from .base import BaseController, FeatureMixin, TemplateMixin, TraceMixin, WaveformMixin # noqa logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # CLI utils -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ DEBUG = False if '--debug' in sys.argv: # pragma: no cover @@ -53,19 +47,20 @@ sys.argv.remove('--lprof') -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Set up logging with the CLI tool -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def exceptionHandler(exception_type, exception, traceback): # pragma: no cover tb = ''.join(format_exception(exception_type, exception, traceback)) - logger.error("An error has occurred (%s): %s\n%s", exception_type.__name__, exception, tb) + logger.error('An error has occurred (%s): %s\n%s', exception_type.__name__, exception, tb) @contextmanager def capture_exceptions(): # pragma: no cover """Log exceptions instead of crashing the GUI, and display an error dialog on errors.""" - logger.debug("Start capturing exceptions.") + logger.debug('Start capturing exceptions.') # Add a custom exception hook. excepthook = sys.excepthook @@ -84,12 +79,13 @@ def capture_exceptions(): # pragma: no cover # Remove the dialog exception handler. logging.getLogger('phy').removeHandler(handler) - logger.debug("Stop capturing exceptions.") + logger.debug('Stop capturing exceptions.') -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Root CLI tool -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + @click.group() @click.version_option(version=__version_git__) @@ -102,25 +98,30 @@ def phycli(ctx): add_default_handler(level='DEBUG' if DEBUG else 'INFO', logger=logging.getLogger('mtscomp')) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # GUI command wrapper -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ def _gui_command(f): """Command options for GUI commands.""" f = click.option( - '--clear-cache/--no-clear-cache', default=False, - help="Clear the .phy cache in the data directory.")(f) + '--clear-cache/--no-clear-cache', + default=False, + help='Clear the .phy cache in the data directory.', + )(f) f = click.option( - '--clear-state/--no-clear-state', default=False, - help="Clear the GUI state in `~/.phy/` and in `.phy`.")(f) + '--clear-state/--no-clear-state', + default=False, + help='Clear the GUI state in `~/.phy/` and in `.phy`.', + )(f) return f -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Raw data GUI -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + @phycli.command('trace-gui') # pragma: no cover @click.argument('dat-path', type=click.Path(exists=True)) @@ -134,15 +135,17 @@ def _gui_command(f): def cli_trace_gui(ctx, dat_path, **kwargs): """Launch the trace GUI on a raw data file.""" from .trace.gui import trace_gui + with capture_exceptions(): kwargs['n_channels_dat'] = kwargs.pop('n_channels') kwargs['order'] = 'F' if kwargs.pop('fortran', None) else None trace_gui(dat_path, **kwargs) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Template GUI -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + @phycli.command('template-gui') # pragma: no cover @click.argument('params-path', type=click.Path(exists=True)) @@ -151,10 +154,12 @@ def cli_trace_gui(ctx, dat_path, **kwargs): def cli_template_gui(ctx, params_path, **kwargs): """Launch the template GUI on a params.py file.""" from .template.gui import template_gui + prof = __builtins__.get('profile', None) with capture_exceptions(): if prof: from phy.utils.profiling import _profile + return _profile(prof, 'template_gui(params_path)', globals(), locals()) template_gui(params_path, **kwargs) @@ -165,12 +170,14 @@ def cli_template_gui(ctx, params_path, **kwargs): def cli_template_describe(ctx, params_path): """Describe a template file.""" from .template.gui import template_describe + template_describe(params_path) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Kwik GUI -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + # Create the `phy cluster-manual file.kwik` command. @phycli.command('kwik-gui') # pragma: no cover @@ -182,6 +189,7 @@ def cli_template_describe(ctx, params_path): def cli_kwik_gui(ctx, path, channel_group=None, clustering=None, **kwargs): """Launch the Kwik GUI on a Kwik file.""" from .kwik.gui import kwik_gui + with capture_exceptions(): assert path kwik_gui(path, channel_group=channel_group, clustering=clustering, **kwargs) @@ -195,13 +203,15 @@ def cli_kwik_gui(ctx, path, channel_group=None, clustering=None, **kwargs): def cli_kwik_describe(ctx, path, channel_group=0, clustering='main'): """Describe a Kwik file.""" from .kwik.gui import kwik_describe + assert path kwik_describe(path, channel_group=channel_group, clustering=clustering) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Conversion -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + @phycli.command('alf-convert') @click.argument('subdirs', nargs=-1, type=click.Path(exists=True, file_okay=False, dir_okay=True)) @@ -227,9 +237,10 @@ def cli_alf_convert(ctx, subdirs, out_dir): c.convert(out_dir) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Waveform extraction -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + @phycli.command('extract-waveforms') @click.argument('params-path', type=click.Path(exists=True)) @@ -237,11 +248,13 @@ def cli_alf_convert(ctx, subdirs, out_dir): @click.option('--nc', type=int, default=16) @click.pass_context def template_extract_waveforms( - ctx, params_path, n_spikes_per_cluster, nc=None): # pragma: no cover + ctx, params_path, n_spikes_per_cluster, nc=None +): # pragma: no cover """Extract spike waveforms.""" from phylib.io.model import load_model model = load_model(params_path) model.save_spikes_subset_waveforms( - max_n_spikes_per_template=n_spikes_per_cluster, max_n_channels=nc) + max_n_spikes_per_template=n_spikes_per_cluster, max_n_channels=nc + ) model.close() diff --git a/phy/apps/base.py b/phy/apps/base.py index 8a1698928..84a19dcd3 100644 --- a/phy/apps/base.py +++ b/phy/apps/base.py @@ -1,35 +1,44 @@ -# -*- coding: utf-8 -*- - """Base controller to make clustering GUIs.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ -from functools import partial import inspect import logging import os -from pathlib import Path import shutil +from functools import partial +from pathlib import Path import numpy as np -from scipy.signal import butter, lfilter - from phylib import _add_log_file from phylib.io.array import SpikeSelector, _flatten from phylib.stats import correlograms, firing_rate -from phylib.utils import Bunch, emit, connect, unconnect +from phylib.utils import Bunch, connect, emit, unconnect from phylib.utils._misc import write_tsv +from scipy.signal import butter, lfilter from phy.cluster._utils import RotatingProperty from phy.cluster.supervisor import Supervisor -from phy.cluster.views.base import ManualClusteringView, BaseColorView from phy.cluster.views import ( - WaveformView, FeatureView, TraceView, TraceImageView, CorrelogramView, AmplitudeView, - ScatterView, ProbeView, RasterView, TemplateView, ISIView, FiringRateView, ClusterScatterView, - select_traces) + AmplitudeView, + ClusterScatterView, + CorrelogramView, + FeatureView, + FiringRateView, + ISIView, + ProbeView, + RasterView, + ScatterView, + TemplateView, + TraceImageView, + TraceView, + WaveformView, + select_traces, +) +from phy.cluster.views.base import BaseColorView, ManualClusteringView from phy.cluster.views.trace import _iter_spike_waveforms from phy.gui import GUI from phy.gui.gui import _prompt_save @@ -42,9 +51,10 @@ logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Utils -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _concatenate_parents_attributes(cls, name): """Return the concatenation of class attributes of a given name among all parents of a @@ -54,7 +64,7 @@ def _concatenate_parents_attributes(cls, name): class Selection(Bunch): def __init__(self, controller): - super(Selection, self).__init__() + super().__init__() self.controller = controller @property @@ -67,19 +77,20 @@ class StatusBarHandler(logging.Handler): def __init__(self, gui): self.gui = gui - super(StatusBarHandler, self).__init__() + super().__init__() def emit(self, record): self.gui.status_message = self.format(record) -#-------------------------------------------------------------------------- +# -------------------------------------------------------------------------- # Raw data filtering -#-------------------------------------------------------------------------- +# -------------------------------------------------------------------------- + class RawDataFilter(RotatingProperty): def __init__(self): - super(RawDataFilter, self).__init__() + super().__init__() self.add('raw', lambda x, axis=None: x) def add_default_filter(self, sample_rate): @@ -92,6 +103,7 @@ def high_pass(arr, axis=0): arr = lfilter(b, a, arr, axis=axis) arr = np.flip(arr, axis=axis) return arr + self.set('high_pass') def add_filter(self, fun=None, name=None): @@ -99,7 +111,7 @@ def add_filter(self, fun=None, name=None): if fun is None: # pragma: no cover return partial(self.add_filter, name=name) name = name or fun.__name__ - logger.debug("Add filter `%s`.", name) + logger.debug('Add filter `%s`.', name) self.add(name, fun) def apply(self, arr, axis=None, name=None): @@ -107,31 +119,31 @@ def apply(self, arr, axis=None, name=None): self.set(name or self.current) fun = self.get() if fun: - logger.log(5, "Applying filter `%s` to raw data.", self.current) + logger.log(5, 'Applying filter `%s` to raw data.', self.current) arrf = fun(arr, axis=axis) assert arrf.shape == arr.shape arr = arrf return arr -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # View mixins -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + -class WaveformMixin(object): +class WaveformMixin: n_spikes_waveforms = 100 batch_size_waveforms = 10 _state_params = ( - 'n_spikes_waveforms', 'batch_size_waveforms', + 'n_spikes_waveforms', + 'batch_size_waveforms', ) _new_views = ('WaveformView',) # Map an amplitude type to a method name. - _amplitude_functions = ( - ('raw', 'get_spike_raw_amplitudes'), - ) + _amplitude_functions = (('raw', 'get_spike_raw_amplitudes'),) _waveform_functions = ( ('waveforms', '_get_waveforms'), @@ -179,8 +191,8 @@ def get_mean_spike_raw_amplitudes(self, cluster_id): return np.mean(self.get_spike_raw_amplitudes(spike_ids)) def _get_waveforms_with_n_spikes( - self, cluster_id, n_spikes_waveforms, current_filter=None): - + self, cluster_id, n_spikes_waveforms, current_filter=None + ): # HACK: we pass self.raw_data_filter.current_filter so that it is cached properly. pos = self.model.channel_positions @@ -188,11 +200,14 @@ def _get_waveforms_with_n_spikes( if self.model.spike_waveforms is not None: subset_spikes = self.model.spike_waveforms.spike_ids spike_ids = self.selector( - n_spikes_waveforms, [cluster_id], subset_spikes=subset_spikes) + n_spikes_waveforms, [cluster_id], subset_spikes=subset_spikes + ) # Or keep spikes from a subset of the chunks for performance reasons (decompression will # happen on the fly here). else: - spike_ids = self.selector(n_spikes_waveforms, [cluster_id], subset_chunks=True) + spike_ids = self.selector( + n_spikes_waveforms, [cluster_id], subset_chunks=True + ) # Get the best channels. channel_ids = self.get_best_channels(cluster_id) @@ -217,23 +232,27 @@ def _get_waveforms_with_n_spikes( def _get_waveforms(self, cluster_id): """Return a selection of waveforms for a cluster.""" return self._get_waveforms_with_n_spikes( - cluster_id, self.n_spikes_waveforms, current_filter=self.raw_data_filter.current) + cluster_id, + self.n_spikes_waveforms, + current_filter=self.raw_data_filter.current, + ) def _get_mean_waveforms(self, cluster_id, current_filter=None): """Get the mean waveform of a cluster on its best channels.""" b = self._get_waveforms(cluster_id) if b.data is not None: b.data = b.data.mean(axis=0)[np.newaxis, ...] - b['alpha'] = 1. + b['alpha'] = 1.0 return b def _set_view_creator(self): - super(WaveformMixin, self)._set_view_creator() + super()._set_view_creator() self.view_creator['WaveformView'] = self.create_waveform_view def _get_waveforms_dict(self): waveform_functions = _concatenate_parents_attributes( - self.__class__, '_waveform_functions') + self.__class__, '_waveform_functions' + ) return {name: getattr(self, method) for name, method in waveform_functions} def create_waveform_view(self): @@ -255,7 +274,10 @@ def on_view_attached(view_, gui): # NOTE: this callback function is called in WaveformView.attach(). @view.actions.add( - alias='wn', prompt=True, prompt_default=lambda: str(self.n_spikes_waveforms)) + alias='wn', + prompt=True, + prompt_default=lambda: str(self.n_spikes_waveforms), + ) def change_n_spikes_waveforms(n_spikes_waveforms): """Change the number of spikes displayed in the waveform view.""" self.n_spikes_waveforms = n_spikes_waveforms @@ -271,19 +293,18 @@ def on_close_view(view_, gui): return view -class FeatureMixin(object): +class FeatureMixin: n_spikes_features = 2500 n_spikes_features_background = 2500 _state_params = ( - 'n_spikes_features', 'n_spikes_features_background', + 'n_spikes_features', + 'n_spikes_features_background', ) _new_views = ('FeatureView',) - _amplitude_functions = ( - ('feature', 'get_spike_feature_amplitudes'), - ) + _amplitude_functions = (('feature', 'get_spike_feature_amplitudes'),) _cached = ( '_get_features', @@ -291,7 +312,8 @@ class FeatureMixin(object): ) def get_spike_feature_amplitudes( - self, spike_ids, channel_id=None, channel_ids=None, pc=None, **kwargs): + self, spike_ids, channel_id=None, channel_ids=None, pc=None, **kwargs + ): """Return the features for the specified channel and PC.""" if self.model.features is None: return @@ -300,11 +322,11 @@ def get_spike_feature_amplitudes( if features is None: # pragma: no cover return assert features.shape[0] == len(spike_ids) - logger.log(5, "Show channel %s and PC %s in amplitude view.", channel_id, pc) + logger.log(5, 'Show channel %s and PC %s in amplitude view.', channel_id, pc) return features[:, 0, pc or 0] def create_amplitude_view(self): - view = super(FeatureMixin, self).create_amplitude_view() + view = super().create_amplitude_view() if self.model.features is None: return view @@ -338,7 +360,7 @@ def _get_feature_view_spike_ids(self, cluster_id=None, load_all=False): assert len(spike_ids) spike_ids = np.intersect1d(spike_ids, self.model.spike_waveforms.spike_ids) if len(spike_ids) == 0: - logger.debug("empty spikes for cluster %s", str(cluster_id)) + logger.debug('empty spikes for cluster %s', str(cluster_id)) return spike_ids # Retrieve features from the self.model.features array. elif self.model.features is not None: @@ -357,9 +379,8 @@ def _get_feature_view_spike_times(self, cluster_id=None, load_all=False): return spike_times = self._get_spike_times_reordered(spike_ids) return Bunch( - data=spike_times, - spike_ids=spike_ids, - lim=(0., self.model.duration)) + data=spike_times, spike_ids=spike_ids, lim=(0.0, self.model.duration) + ) def _get_spike_features(self, spike_ids, channel_ids): if len(spike_ids) == 0: # pragma: no cover @@ -372,7 +393,11 @@ def _get_spike_features(self, spike_ids, channel_ids): assert np.isnan(data).sum() == 0 channel_labels = self._get_channel_labels(channel_ids) return Bunch( - data=data, spike_ids=spike_ids, channel_ids=channel_ids, channel_labels=channel_labels) + data=data, + spike_ids=spike_ids, + channel_ids=channel_ids, + channel_labels=channel_labels, + ) def _get_features(self, cluster_id=None, channel_ids=None, load_all=False): """Return the features of a given cluster on specified channels.""" @@ -386,12 +411,15 @@ def _get_features(self, cluster_id=None, channel_ids=None, load_all=False): return self._get_spike_features(spike_ids, channel_ids) def create_feature_view(self): - if self.model.features is None and getattr(self.model, 'spike_waveforms', None) is None: + if ( + self.model.features is None + and getattr(self.model, 'spike_waveforms', None) is None + ): # NOTE: we can still construct the feature view when there are spike waveforms. return view = FeatureView( features=self._get_features, - attributes={'time': self._get_feature_view_spike_times} + attributes={'time': self._get_feature_view_spike_times}, ) @connect @@ -420,11 +448,11 @@ def on_close_view(view_, gui): return view def _set_view_creator(self): - super(FeatureMixin, self)._set_view_creator() + super()._set_view_creator() self.view_creator['FeatureView'] = self.create_feature_view -class TemplateMixin(object): +class TemplateMixin: """Support templates. The model needs to implement specific properties and methods. @@ -443,13 +471,9 @@ class TemplateMixin(object): _new_views = ('TemplateView',) - _amplitude_functions = ( - ('template', 'get_spike_template_amplitudes'), - ) + _amplitude_functions = (('template', 'get_spike_template_amplitudes'),) - _waveform_functions = ( - ('templates', '_get_template_waveforms'), - ) + _waveform_functions = (('templates', '_get_template_waveforms'),) _cached = ( 'get_amplitudes', @@ -467,10 +491,10 @@ class TemplateMixin(object): ) def __init__(self, *args, **kwargs): - super(TemplateMixin, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def _get_amplitude_functions(self): - out = super(TemplateMixin, self)._get_amplitude_functions() + out = super()._get_amplitude_functions() if getattr(self.model, 'template_features', None) is not None: out['template_feature'] = self.get_spike_template_features return out @@ -507,7 +531,7 @@ def get_cluster_amplitude(self, cluster_id): def _set_cluster_metrics(self): """Add an amplitude column in the cluster view.""" - super(TemplateMixin, self)._set_cluster_metrics() + super()._set_cluster_metrics() self.cluster_metrics['amp'] = self.get_cluster_amplitude def get_spike_template_amplitudes(self, spike_ids, **kwargs): @@ -554,7 +578,9 @@ def _get_template_waveforms(self, cluster_id): masks = count / float(count.max()) masks = np.tile(masks.reshape((-1, 1)), (1, len(channel_ids))) # Get all templates from which this cluster stems from. - templates = [self.model.get_template(template_id) for template_id in template_ids] + templates = [ + self.model.get_template(template_id) for template_id in template_ids + ] # Construct the waveforms array. ns = self.model.n_samples_waveforms data = np.zeros((len(template_ids), ns, self.model.n_channels)) @@ -567,7 +593,9 @@ def _get_template_waveforms(self, cluster_id): channel_ids=channel_ids, channel_labels=self._get_channel_labels(channel_ids), channel_positions=pos[channel_ids], - masks=masks, alpha=1.) + masks=masks, + alpha=1.0, + ) def _get_all_templates(self, cluster_ids): """Get the template waveforms of a set of clusters.""" @@ -581,7 +609,7 @@ def _get_all_templates(self, cluster_ids): return out def _set_view_creator(self): - super(TemplateMixin, self)._set_view_creator() + super()._set_view_creator() self.view_creator['TemplateView'] = self.create_template_view def create_template_view(self): @@ -596,27 +624,31 @@ def create_template_view(self): return view -class TraceMixin(object): - +class TraceMixin: _new_views = ('TraceView', 'TraceImageView') waveform_duration = 1.0 # in milliseconds def _get_traces(self, interval, show_all_spikes=False): """Get traces and spike waveforms.""" traces_interval = select_traces( - self.model.traces, interval, sample_rate=self.model.sample_rate) + self.model.traces, interval, sample_rate=self.model.sample_rate + ) # Filter the loaded traces. traces_interval = self.raw_data_filter.apply(traces_interval, axis=0) out = Bunch(data=traces_interval) - out.waveforms = list(_iter_spike_waveforms( - interval=interval, - traces_interval=traces_interval, - model=self.model, - supervisor=self.supervisor, - n_samples_waveforms=int(round(1e-3 * self.waveform_duration * self.model.sample_rate)), - get_best_channels=self.get_channel_amplitudes, - show_all_spikes=show_all_spikes, - )) + out.waveforms = list( + _iter_spike_waveforms( + interval=interval, + traces_interval=traces_interval, + model=self.model, + supervisor=self.supervisor, + n_samples_waveforms=int( + round(1e-3 * self.waveform_duration * self.model.sample_rate) + ), + get_best_channels=self.get_channel_amplitudes, + show_all_spikes=show_all_spikes, + ) + ) return out def _trace_spike_times(self): @@ -646,6 +678,7 @@ def create_trace_view(self): # Update the get_traces() function with show_all_spikes. def _get_traces(interval): return self._get_traces(interval, show_all_spikes=view.show_all_spikes) + view.traces = _get_traces view.ex_status = self.raw_data_filter.current @@ -697,16 +730,17 @@ def on_close_view(view_, gui): return view def _set_view_creator(self): - super(TraceMixin, self)._set_view_creator() + super()._set_view_creator() self.view_creator['TraceView'] = self.create_trace_view self.view_creator['TraceImageView'] = self.create_trace_image_view -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Base Controller -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + -class BaseController(object): +class BaseController: """Base controller for manual clustering GUI. Constructor @@ -821,8 +855,7 @@ class BaseController(object): # Pairs (amplitude_type_name, method_name) where amplitude methods return spike amplitudes # of a given type. - _amplitude_functions = ( - ) + _amplitude_functions = () n_spikes_correlograms = 100000 @@ -832,7 +865,8 @@ class BaseController(object): # Controller attributes to load/save in the GUI state. _state_params = ( - 'n_spikes_amplitudes', 'n_spikes_correlograms', + 'n_spikes_amplitudes', + 'n_spikes_correlograms', 'raw_data_filter_name', ) @@ -853,8 +887,12 @@ class BaseController(object): # Views to load by default. _new_views = ( - 'ClusterScatterView', 'CorrelogramView', 'AmplitudeView', - 'ISIView', 'FiringRateView', 'ProbeView', + 'ClusterScatterView', + 'CorrelogramView', + 'AmplitudeView', + 'ISIView', + 'FiringRateView', + 'ProbeView', ) default_shortcuts = { @@ -864,10 +902,15 @@ class BaseController(object): default_snippets = {} def __init__( - self, dir_path=None, config_dir=None, model=None, - clear_cache=None, clear_state=None, - enable_threading=True, **kwargs): - + self, + dir_path=None, + config_dir=None, + model=None, + clear_cache=None, + clear_state=None, + enable_threading=True, + **kwargs, + ): self._enable_threading = enable_threading assert dir_path @@ -878,7 +921,9 @@ def __init__( _add_log_file(Path(dir_path) / 'phy.log') # Create or reuse a Model instance (any object) - self.model = self._create_model(dir_path=dir_path, **kwargs) if model is None else model + self.model = ( + self._create_model(dir_path=dir_path, **kwargs) if model is None else model + ) # Set up the cache. self._set_cache(clear_cache) @@ -899,7 +944,9 @@ def __init__( # The controller.default_views can be set by the child class, otherwise it is computed # by concatenating all parents _new_views. if getattr(self, 'default_views', None) is None: - self.default_views = _concatenate_parents_attributes(self.__class__, '_new_views') + self.default_views = _concatenate_parents_attributes( + self.__class__, '_new_views' + ) self._async_callers = {} self.config_dir = config_dir @@ -907,15 +954,19 @@ def __init__( if clear_state: self._clear_state() - self.selection = Selection(self) # keep track of selected clusters, spikes, channels, etc. + self.selection = Selection( + self + ) # keep track of selected clusters, spikes, channels, etc. # Attach plugins before setting up the supervisor, so that plugins # can register callbacks to events raised during setup. # For example, 'request_cluster_metrics' to specify custom metrics # in the cluster and similarity views. self.attached_plugins = attach_plugins( - self, config_dir=config_dir, - plugins=kwargs.get('plugins', None), dirs=kwargs.get('plugin_dirs', None), + self, + config_dir=config_dir, + plugins=kwargs.get('plugins'), + dirs=kwargs.get('plugin_dirs'), ) # Cache the methods specified in self._memcached and self._cached. All method names @@ -938,19 +989,19 @@ def _create_model(self, dir_path=None, **kwargs): return def _clear_cache(self): - logger.warn("Deleting the cache directory %s.", self.cache_dir) + logger.warn('Deleting the cache directory %s.', self.cache_dir) shutil.rmtree(self.cache_dir, ignore_errors=True) def _clear_state(self): """Clear the global and local GUI state files.""" state_path = _gui_state_path(self.gui_name, config_dir=self.config_dir) if state_path.exists(): - logger.warning("Deleting %s.", state_path) + logger.warning('Deleting %s.', state_path) state_path.unlink() local_path = self.cache_dir / 'state.json' if local_path.exists(): local_path.unlink() - logger.warning("Deleting %s.", local_path) + logger.warning('Deleting %s.', local_path) def _set_cache(self, clear_cache=None): """Set up the cache, clear it if required, and create the Context instance.""" @@ -969,7 +1020,9 @@ def _set_view_creator(self): 'ClusterScatterView': self.create_cluster_scatter_view, 'CorrelogramView': self.create_correlogram_view, 'ISIView': self._make_histogram_view(ISIView, self._get_isi), - 'FiringRateView': self._make_histogram_view(FiringRateView, self._get_firing_rate), + 'FiringRateView': self._make_histogram_view( + FiringRateView, self._get_firing_rate + ), 'AmplitudeView': self.create_amplitude_view, 'ProbeView': self.create_probe_view, 'RasterView': self.create_raster_view, @@ -977,8 +1030,10 @@ def _set_view_creator(self): } # Spike attributes. for name, arr in getattr(self.model, 'spike_attributes', {}).items(): - view_name = 'Spike%sView' % name.title() - self.view_creator[view_name] = self._make_spike_attributes_view(view_name, name, arr) + view_name = f'Spike{name.title()}View' + self.view_creator[view_name] = self._make_spike_attributes_view( + view_name, name, arr + ) def _set_cluster_metrics(self): """Set the cluster metrics dictionary with some default metrics.""" @@ -1035,7 +1090,8 @@ def _set_selector(self): def spikes_per_cluster(cluster_id): return self.supervisor.clustering.spikes_per_cluster.get( - cluster_id, np.array([], dtype=np.int64)) + cluster_id, np.array([], dtype=np.int64) + ) try: chunk_bounds = self.model.traces.chunk_bounds @@ -1046,7 +1102,8 @@ def spikes_per_cluster(cluster_id): get_spikes_per_cluster=spikes_per_cluster, spike_times=self.model.spike_samples, # NOTE: chunk_bounds is in samples, not seconds chunk_bounds=chunk_bounds, - n_chunks_kept=self.n_chunks_kept) + n_chunks_kept=self.n_chunks_kept, + ) def _cache_methods(self): """Cache methods as specified in `self._memcached` and `self._cached`.""" @@ -1060,12 +1117,13 @@ def _get_channel_labels(self, channel_ids=None): """Return the labels of a list of channels.""" if channel_ids is None: channel_ids = np.arange(self.model.n_channels) - if (hasattr(self.model, 'channel_mapping') and - getattr(self.model, 'show_mapped_channels', self.default_show_mapped_channels)): + if hasattr(self.model, 'channel_mapping') and getattr( + self.model, 'show_mapped_channels', self.default_show_mapped_channels + ): channel_labels = self.model.channel_mapping[channel_ids] else: channel_labels = channel_ids - return ['%d' % ch for ch in channel_labels] + return [f'{ch}' for ch in channel_labels] # Internal view methods # ------------------------------------------------------------------------- @@ -1097,6 +1155,7 @@ def _update_plot(): view.set_cluster_ids(self.supervisor.shown_cluster_ids) # Replot the view entirely. view.plot() + if is_async: ac.set(_update_plot) else: @@ -1169,8 +1228,12 @@ def _save_cluster_info(self): for d in cluster_info: d['cluster_id'] = d.pop('id') write_tsv( - self.dir_path / 'cluster_info.tsv', cluster_info, - first_field='cluster_id', exclude_fields=('is_masked',), n_significant_figures=8) + self.dir_path / 'cluster_info.tsv', + cluster_info, + first_field='cluster_id', + exclude_fields=('is_masked',), + n_significant_figures=8, + ) # Model methods # ------------------------------------------------------------------------- @@ -1196,19 +1259,18 @@ def get_best_channel_label(self, cluster_id): def get_best_channels(self, cluster_id): # pragma: no cover """Return the best channels of a given cluster. To be overriden.""" logger.warning( - "This method should be overriden and return a non-empty list of best channels.") + 'This method should be overriden and return a non-empty list of best channels.' + ) return [] def get_channel_amplitudes(self, cluster_id): # pragma: no cover """Return the best channels of a given cluster along with their relative amplitudes. To be overriden.""" - logger.warning( - "This method should be overriden.") + logger.warning('This method should be overriden.') return [] def get_channel_shank(self, cluster_id): - """Return the shank of a cluster's best channel, if the channel_shanks array is available. - """ + """Return the shank of a cluster's best channel, if the channel_shanks array is available.""" best_channel_id = self.get_best_channel(cluster_id) return self.model.channel_shanks[best_channel_id] @@ -1220,8 +1282,10 @@ def get_probe_depth(self, cluster_id): def get_clusters_on_channel(self, channel_id): """Return all clusters which have the specified channel among their best channels.""" return [ - cluster_id for cluster_id in self.supervisor.clustering.cluster_ids - if channel_id in self.get_best_channels(cluster_id)] + cluster_id + for cluster_id in self.supervisor.clustering.cluster_ids + if channel_id in self.get_best_channels(cluster_id) + ] # Default similarity functions # ------------------------------------------------------------------------- @@ -1243,8 +1307,10 @@ def peak_channel_similarity(self, cluster_id): """ ch = self.get_best_channel(cluster_id) return [ - (other, 1.) for other in self.supervisor.clustering.cluster_ids - if ch in self.get_best_channels(other)] + (other, 1.0) + for other in self.supervisor.clustering.cluster_ids + if ch in self.get_best_channels(other) + ] # Public spike methods # ------------------------------------------------------------------------- @@ -1269,8 +1335,10 @@ def get_background_spike_ids(self, n=None): def _get_spike_times_reordered(self, spike_ids): """Get spike times, reordered if needed.""" spike_times = self.model.spike_times - if (self.selection.get('do_reorder', None) and - getattr(self.model, 'spike_times_reordered', None) is not None): + if ( + self.selection.get('do_reorder', None) + and getattr(self.model, 'spike_times_reordered', None) is not None + ): spike_times = self.model.spike_times_reordered spike_times = spike_times[spike_ids] return spike_times @@ -1279,7 +1347,8 @@ def _get_amplitude_functions(self): """Return a dictionary mapping amplitude names to corresponding methods.""" # Concatenation of all _amplitude_functions attributes in the class hierarchy. amplitude_functions = _concatenate_parents_attributes( - self.__class__, '_amplitude_functions') + self.__class__, '_amplitude_functions' + ) return {name: getattr(self, method) for name, method in amplitude_functions} def _get_amplitude_spike_ids(self, cluster_id, load_all=False): @@ -1305,7 +1374,9 @@ def _amplitude_getter(self, cluster_ids, name=None, load_all=False): out = [] n = self.n_spikes_amplitudes if not load_all else None # Find the first cluster, used to determine the best channels. - first_cluster = next(cluster_id for cluster_id in cluster_ids if cluster_id is not None) + first_cluster = next( + cluster_id for cluster_id in cluster_ids if cluster_id is not None + ) # Best channels of the first cluster. channel_ids = self.get_best_channels(first_cluster) # Best channel of the first cluster. @@ -1332,11 +1403,19 @@ def _amplitude_getter(self, cluster_ids, name=None, load_all=False): if cluster_id is not None: # Cluster spikes. spike_ids = self.get_spike_ids( - cluster_id, n=n, subset_spikes=subset_spikes, subset_chunks=subset_chunks) + cluster_id, + n=n, + subset_spikes=subset_spikes, + subset_chunks=subset_chunks, + ) else: # Background spikes. spike_ids = self.selector( - n, other_clusters, subset_spikes=subset_spikes, subset_chunks=subset_chunks) + n, + other_clusters, + subset_spikes=subset_spikes, + subset_chunks=subset_chunks, + ) # Get the spike times. spike_times = self._get_spike_times_reordered(spike_ids) if name in ('feature', 'raw'): @@ -1346,23 +1425,30 @@ def _amplitude_getter(self, cluster_ids, name=None, load_all=False): pc = self.selection.get('feature_pc', None) # Call the spike amplitude getter function. amplitudes = f( - spike_ids, channel_ids=channel_ids, channel_id=channel_id, pc=pc, - first_cluster=first_cluster) + spike_ids, + channel_ids=channel_ids, + channel_id=channel_id, + pc=pc, + first_cluster=first_cluster, + ) if amplitudes is None: continue assert amplitudes.shape == spike_ids.shape == spike_times.shape - out.append(Bunch( - amplitudes=amplitudes, - spike_ids=spike_ids, - spike_times=spike_times, - )) + out.append( + Bunch( + amplitudes=amplitudes, + spike_ids=spike_ids, + spike_times=spike_times, + ) + ) return out def create_amplitude_view(self): """Create the amplitude view.""" amplitudes_dict = { name: partial(self._amplitude_getter, name=name) - for name in sorted(self._get_amplitude_functions())} + for name in sorted(self._get_amplitude_functions()) + } if not amplitudes_dict: return # NOTE: we disable raw amplitudes for now as they're either too slow to load, @@ -1479,15 +1565,21 @@ def _get_correlograms(self, cluster_ids, bin_size, window_size): st = self.model.spike_times[spike_ids] sc = self.supervisor.clustering.spike_clusters[spike_ids] return correlograms( - st, sc, sample_rate=self.model.sample_rate, cluster_ids=cluster_ids, - bin_size=bin_size, window_size=window_size) + st, + sc, + sample_rate=self.model.sample_rate, + cluster_ids=cluster_ids, + bin_size=bin_size, + window_size=window_size, + ) def _get_correlograms_rate(self, cluster_ids, bin_size): """Return the baseline firing rate of the cross- and auto-correlograms of clusters.""" spike_ids = self.selector(self.n_spikes_correlograms, cluster_ids) sc = self.supervisor.clustering.spike_clusters[spike_ids] return firing_rate( - sc, cluster_ids=cluster_ids, bin_size=bin_size, duration=self.model.duration) + sc, cluster_ids=cluster_ids, bin_size=bin_size, duration=self.model.duration + ) def create_correlogram_view(self): """Create a correlogram view.""" @@ -1513,8 +1605,10 @@ def create_probe_view(self): def _make_histogram_view(self, view_cls, method): """Return a function that creates a HistogramView of a given class.""" + def _make(): return view_cls(cluster_stat=method) + return _make def _get_isi(self, cluster_id): @@ -1534,6 +1628,7 @@ def _get_firing_rate(self, cluster_id): def _make_spike_attributes_view(self, view_name, name, arr): """Create a special class deriving from ScatterView for each spike attribute.""" + def coords(cluster_ids, load_all=False): n = self.n_spikes_amplitudes if not load_all else None bunchs = [] @@ -1553,6 +1648,7 @@ def coords(cluster_ids, load_all=False): def _make(): return view_cls(coords=coords) + return _make # IPython View @@ -1563,8 +1659,12 @@ def create_ipython_view(self): view = IPythonView() view.start_kernel() view.inject( - controller=self, c=self, m=self.model, s=self.supervisor, - emit=emit, connect=connect, + controller=self, + c=self, + m=self.model, + s=self.supervisor, + emit=emit, + connect=connect, ) return view @@ -1577,6 +1677,7 @@ def at_least_one_view(self, view_name): To be called before creating a GUI. """ + @connect(sender=self) def on_gui_ready(sender, gui): # Add a view automatically. @@ -1584,14 +1685,17 @@ def on_gui_ready(sender, gui): gui.create_and_add_view(view_name) def create_misc_actions(self, gui): - # Toggle spike reorder. @gui.view_actions.add( shortcut=self.default_shortcuts['toggle_spike_reorder'], - checkable=True, checked=False) + checkable=True, + checked=False, + ) def toggle_spike_reorder(checked): """Toggle spike time reordering.""" - logger.debug("%s spike time reordering.", 'Enable' if checked else 'Disable') + logger.debug( + '%s spike time reordering.', 'Enable' if checked else 'Disable' + ) emit('toggle_spike_reorder', self, checked) # Action to switch the raw data filter inthe trace and waveform views. @@ -1608,7 +1712,9 @@ def switch_raw_data_filter(): # Update the waveform view. for v in gui.list_views(WaveformView): if v.auto_update: - v.on_select_threaded(self.supervisor, self.supervisor.selected, gui=gui) + v.on_select_threaded( + self.supervisor, self.supervisor.selected, gui=gui + ) v.ex_status = filter_name v.update_status() @@ -1623,11 +1729,13 @@ def _add_default_color_schemes(self, view): None: 3, 'unsorted': 3, } - logger.debug("Adding default color schemes to %s.", view.name) + logger.debug('Adding default color schemes to %s.', view.name) def group_index(cluster_id): group = self.supervisor.cluster_meta.get('group', cluster_id) - return group_colors.get(group, 0) # TODO: better handling of colors for custom groups + return group_colors.get( + group, 0 + ) # TODO: better handling of colors for custom groups depth = self.supervisor.cluster_metrics['depth'] fr = self.supervisor.cluster_metrics['fr'] @@ -1640,8 +1748,13 @@ def group_index(cluster_id): ] for name, colormap, fun, categorical, logarithmic in schemes: view.add_color_scheme( - name=name, fun=fun, cluster_ids=self.supervisor.clustering.cluster_ids, - colormap=colormap, categorical=categorical, logarithmic=logarithmic) + name=name, + fun=fun, + cluster_ids=self.supervisor.clustering.cluster_ids, + colormap=colormap, + categorical=categorical, + logarithmic=logarithmic, + ) # Default color scheme. if not hasattr(view, 'color_scheme_name'): view.color_schemes.set('random') @@ -1663,11 +1776,13 @@ def create_gui(self, default_views=None, **kwargs): subtitle=str(self.dir_path), config_dir=self.config_dir, local_path=self.cache_dir / 'state.json', - default_state_path=Path(inspect.getfile(self.__class__)).parent / 'static/state.json', + default_state_path=Path(inspect.getfile(self.__class__)).parent + / 'static/state.json', view_creator=self.view_creator, default_views=default_views, enable_threading=self._enable_threading, - **kwargs) + **kwargs, + ) # Set all state parameters from the GUI state. state_params = _concatenate_parents_attributes(self.__class__, '_state_params') @@ -1690,8 +1805,13 @@ def on_view_attached(view, gui_): if isinstance(view, ManualClusteringView): # Add auto update button. view.dock.add_button( - name='auto_update', icon='f021', checkable=True, checked=view.auto_update, - event='toggle_auto_update', callback=view.toggle_auto_update) + name='auto_update', + icon='f021', + checkable=True, + checked=view.auto_update, + event='toggle_auto_update', + callback=view.toggle_auto_update, + ) # Show selected clusters when adding new views in the GUI. view.on_select(cluster_ids=self.supervisor.selected_clusters) @@ -1736,12 +1856,11 @@ def on_close(sender): # Save the memcache when closing the GUI. @connect(sender=gui) # noqa def on_close(sender): # noqa - # Gather all GUI state attributes from views that are local and thus need # to be saved in the data directory. for view in gui.views: local_keys = getattr(view, 'local_state_attrs', []) - local_keys = ['%s.%s' % (view.name, key) for key in local_keys] + local_keys = [f'{view.name}.{key}' for key in local_keys] gui.state.add_local_keys(local_keys) # Update the controller params in the GUI state. diff --git a/phy/apps/kwik/__init__.py b/phy/apps/kwik/__init__.py index 3df209f7d..636b8028f 100644 --- a/phy/apps/kwik/__init__.py +++ b/phy/apps/kwik/__init__.py @@ -1,11 +1,10 @@ -# -*- coding: utf-8 -*- # flake8: noqa """Kwik GUI.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ from .gui import KwikController, kwik_describe, kwik_gui # noqa diff --git a/phy/apps/kwik/gui.py b/phy/apps/kwik/gui.py index 870ed6fb8..d1132b519 100644 --- a/phy/apps/kwik/gui.py +++ b/phy/apps/kwik/gui.py @@ -1,27 +1,25 @@ -# -*- coding: utf-8 -*- - """Kwik GUI.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import logging -from pathlib import Path import shutil +from pathlib import Path from tempfile import TemporaryDirectory import numpy as np - from phylib.stats.clusters import get_waveform_amplitude from phylib.utils import Bunch, connect from phylib.utils.geometry import linear_positions -from phy.utils.context import Context -from phy.gui import create_app, run_app -from ..base import WaveformMixin, FeatureMixin, TraceMixin, BaseController from phy.cluster.supervisor import Supervisor +from phy.gui import create_app, run_app +from phy.utils.context import Context + +from ..base import BaseController, FeatureMixin, TraceMixin, WaveformMixin logger = logging.getLogger(__name__) @@ -29,19 +27,20 @@ from klusta.kwik import KwikModel from klusta.launch import cluster except ImportError: # pragma: no cover - logger.debug("Package klusta not installed: the KwikGUI will not work.") + logger.debug('Package klusta not installed: the KwikGUI will not work.') -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Kwik GUI -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _backup(path): """Backup a file.""" assert path.exists() - path_backup = str(path) + '.bak' + path_backup = f'{str(path)}.bak' if not Path(path_backup).exists(): - logger.info("Backup `%s`.", path_backup) + logger.info('Backup `%s`.', path_backup) shutil.copy(str(path), str(path_backup)) @@ -100,9 +99,9 @@ def __init__(self, kwik_path=None, **kwargs): assert kwik_path kwik_path = Path(kwik_path) dir_path = kwik_path.parent - self.channel_group = kwargs.get('channel_group', None) - self.clustering = kwargs.get('clustering', None) - super(KwikController, self).__init__(kwik_path=kwik_path, dir_path=dir_path, **kwargs) + self.channel_group = kwargs.get('channel_group') + self.clustering = kwargs.get('clustering') + super().__init__(kwik_path=kwik_path, dir_path=dir_path, **kwargs) # Internal methods # ------------------------------------------------------------------------- @@ -113,7 +112,7 @@ def _set_cache(self, clear_cache=None): if self.channel_group is not None: self.cache_dir = self.cache_dir / str(self.channel_group) if clear_cache: - logger.warn("Deleting the cache directory %s.", self.cache_dir) + logger.warn('Deleting the cache directory %s.', self.cache_dir) shutil.rmtree(self.cache_dir, ignore_errors=True) self.context = Context(self.cache_dir) @@ -124,7 +123,7 @@ def _create_model(self, **kwargs): model = KwikModelGUI(str(kwik_path), **kwargs) # HACK: handle badly formed channel positions if model.channel_positions.ndim == 1: # pragma: no cover - logger.warning("Unable to read the channel positions, generating mock ones.") + logger.warning('Unable to read the channel positions, generating mock ones.') model.probe.positions = linear_positions(len(model.channel_positions)) return model @@ -158,7 +157,7 @@ def recluster(cluster_ids=None): # Selected clusters. cluster_ids = supervisor.selected spike_ids = self.selector(None, cluster_ids) - logger.info("Running KlustaKwik on %d spikes.", len(spike_ids)) + logger.info('Running KlustaKwik on %d spikes.', len(spike_ids)) # Run KK2 in a temporary directory to avoid side effects. n = 10 @@ -203,8 +202,8 @@ def _get_waveforms(self, cluster_id): def _get_mean_waveforms(self, cluster_id): b = self._get_waveforms(cluster_id).copy() b.data = np.mean(b.data, axis=0)[np.newaxis, ...] - b.masks = np.mean(b.masks, axis=0)[np.newaxis, ...] ** .1 - b['alpha'] = 1. + b.masks = np.mean(b.masks, axis=0)[np.newaxis, ...] ** 0.1 + b['alpha'] = 1.0 return b # Public methods @@ -214,7 +213,7 @@ def get_best_channels(self, cluster_id): """Get the best channels of a given cluster.""" mm = self._get_mean_masks(cluster_id) channel_ids = np.argsort(mm)[::-1] - ind = mm[channel_ids] > .1 + ind = mm[channel_ids] > 0.1 if np.sum(ind) > 0: channel_ids = channel_ids[ind] else: # pragma: no cover @@ -233,16 +232,16 @@ def on_save_clustering(self, sender, spike_clusters, groups, *labels): self._save_cluster_info() -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Kwik commands -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def kwik_gui(path, channel_group=None, clustering=None, **kwargs): # pragma: no cover """Launch the Kwik GUI.""" assert path create_app() - controller = KwikController( - path, channel_group=channel_group, clustering=clustering, **kwargs) + controller = KwikController(path, channel_group=channel_group, clustering=clustering, **kwargs) gui = controller.create_gui() gui.show() run_app() diff --git a/phy/apps/kwik/tests/test_gui.py b/phy/apps/kwik/tests/test_gui.py index c397f6c73..5046af9e7 100644 --- a/phy/apps/kwik/tests/test_gui.py +++ b/phy/apps/kwik/tests/test_gui.py @@ -1,23 +1,22 @@ -# -*- coding: utf-8 -*- - """Testing the Kwik GUI.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import logging -from pathlib import Path import shutil import unittest +from pathlib import Path from phylib.io.datasets import download_test_file from phylib.utils.testing import captured_output from phy.apps.tests.test_base import BaseControllerTests +from phy.cluster.views import WaveformView from phy.plot.tests import key_press + from ..gui import KwikController, kwik_describe -from phy.cluster.views import WaveformView logger = logging.getLogger(__name__) @@ -32,8 +31,12 @@ def _kwik_controller(tempdir, kwik_only=False): shutil.copy(loc_path, tempdir / loc_path.name) kwik_path = tempdir / 'hybrid_10sec.kwik' return KwikController( - kwik_path, channel_group=0, config_dir=tempdir / 'config', - clear_cache=True, enable_threading=False) + kwik_path, + channel_group=0, + config_dir=tempdir / 'config', + clear_cache=True, + enable_threading=False, + ) def test_kwik_describe(qtbot, tempdir): diff --git a/phy/apps/template/__init__.py b/phy/apps/template/__init__.py index d97bbd0e0..19b623477 100644 --- a/phy/apps/template/__init__.py +++ b/phy/apps/template/__init__.py @@ -1,11 +1,9 @@ -# -*- coding: utf-8 -*- - """Template GUI.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ from phylib.io.model import TemplateModel, from_sparse, get_template_params, load_model # noqa from .gui import TemplateController, template_describe, template_gui # noqa diff --git a/phy/apps/template/gui.py b/phy/apps/template/gui.py index 797cdfc96..924d2ff69 100644 --- a/phy/apps/template/gui.py +++ b/phy/apps/template/gui.py @@ -1,18 +1,15 @@ -# -*- coding: utf-8 -*- - """Template GUI.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import logging from operator import itemgetter from pathlib import Path import numpy as np - from phylib import _add_log_file from phylib.io.model import TemplateModel, load_model from phylib.io.traces import MtscompEphysReader @@ -20,22 +17,25 @@ from phy.cluster.views import ScatterView from phy.gui import create_app, run_app -from ..base import WaveformMixin, FeatureMixin, TemplateMixin, TraceMixin, BaseController + +from ..base import BaseController, FeatureMixin, TemplateMixin, TraceMixin, WaveformMixin logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Custom views -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + class TemplateFeatureView(ScatterView): """Scatter view showing the template features.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Template Controller -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + class TemplateController(WaveformMixin, FeatureMixin, TemplateMixin, TraceMixin, BaseController): """Controller for the Template GUI. @@ -81,7 +81,7 @@ class TemplateController(WaveformMixin, FeatureMixin, TemplateMixin, TraceMixin, # ------------------------------------------------------------------------- def _get_waveforms_dict(self): - waveforms_dict = super(TemplateController, self)._get_waveforms_dict() + waveforms_dict = super()._get_waveforms_dict() # Remove waveforms and mean_waveforms if there is no raw data file. if self.model.traces is None and self.model.spike_waveforms is None: waveforms_dict.pop('waveforms', None) @@ -92,7 +92,7 @@ def _create_model(self, dir_path=None, **kwargs): return TemplateModel(dir_path=dir_path, **kwargs) def _set_supervisor(self): - super(TemplateController, self)._set_supervisor() + super()._set_supervisor() supervisor = self.supervisor @@ -107,7 +107,7 @@ def split_init(cluster_ids=None): supervisor.actions.split(s, self.model.spike_templates[s]) def _set_similarity_functions(self): - super(TemplateController, self)._set_similarity_functions() + super()._set_similarity_functions() self.similarity_functions['template'] = self.template_similarity self.similarity = 'template' @@ -139,7 +139,7 @@ def _get_template_features(self, cluster_ids, load_all=False): ] def _set_view_creator(self): - super(TemplateController, self)._set_view_creator() + super()._set_view_creator() self.view_creator['TemplateFeatureView'] = self.create_template_feature_view # Public methods @@ -156,9 +156,9 @@ def get_best_channels(self, cluster_id): def get_channel_amplitudes(self, cluster_id): """Return the channel amplitudes of the best channels of a given cluster.""" template_id = self.get_template_for_cluster(cluster_id) - template = self.model.get_template(template_id, amplitude_threshold=.5) + template = self.model.get_template(template_id, amplitude_threshold=0.5) if not template: # pragma: no cover - return [0], [0.] + return [0], [0.0] m, M = template.amplitude.min(), template.amplitude.max() d = (M - m) if m < M else 1.0 return template.channel_ids, (template.amplitude - m) / d @@ -195,9 +195,10 @@ def create_template_feature_view(self): return TemplateFeatureView(coords=self._get_template_features) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Template commands -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def template_gui(params_path, **kwargs): # pragma: no cover """Launch the Template GUI.""" @@ -210,8 +211,7 @@ def template_gui(params_path, **kwargs): # pragma: no cover # Automatically export spike waveforms when using compressed raw ephys. if model.spike_waveforms is None and isinstance(model.traces, MtscompEphysReader): # TODO: customizable values below. - model.save_spikes_subset_waveforms( - max_n_spikes_per_template=500, max_n_channels=16) + model.save_spikes_subset_waveforms(max_n_spikes_per_template=500, max_n_channels=16) create_app() controller = TemplateController(model=model, dir_path=dir_path, **kwargs) diff --git a/phy/apps/template/tests/test_gui.py b/phy/apps/template/tests/test_gui.py index 7c8626832..c65651ee5 100644 --- a/phy/apps/template/tests/test_gui.py +++ b/phy/apps/template/tests/test_gui.py @@ -1,39 +1,41 @@ -# -*- coding: utf-8 -*- - """Testing the Template GUI.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import logging -from pathlib import Path import re import unittest +from pathlib import Path import numpy as np - -from phylib.io.model import load_model, get_template_params +from phylib.io.model import get_template_params, load_model from phylib.io.tests.conftest import _make_dataset from phylib.utils.testing import captured_output -from phy.apps.tests.test_base import MinimalControllerTests, BaseControllerTests, GlobalViewsTests -from ..gui import ( - template_describe, TemplateController, TemplateFeatureView) +from phy.apps.tests.test_base import BaseControllerTests, GlobalViewsTests, MinimalControllerTests + +from ..gui import TemplateController, TemplateFeatureView, template_describe logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Tests -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _template_controller(tempdir, dir_path, **kwargs): kwargs.update(get_template_params(dir_path / 'params.py')) return TemplateController( - config_dir=tempdir / 'config', plugin_dirs=[plugins_dir()], + config_dir=tempdir / 'config', + plugin_dirs=[plugins_dir()], clear_cache=kwargs.pop('clear_cache', True), - clear_state=True, enable_threading=False, **kwargs) + clear_state=True, + enable_threading=False, + **kwargs, + ) def test_template_describe(qtbot, tempdir): @@ -45,6 +47,7 @@ def test_template_describe(qtbot, tempdir): class TemplateControllerTests(GlobalViewsTests, BaseControllerTests): """Base template controller tests.""" + @classmethod def _create_dataset(cls, tempdir): # pragma: no cover """To be overriden in child classes.""" @@ -78,7 +81,8 @@ def test_template_split_init(self): def test_spike_attribute_views(self): """Open all available spike attribute views.""" view_names = [ - name for name in self.controller.view_creator.keys() if name.startswith('Spike')] + name for name in self.controller.view_creator.keys() if name.startswith('Spike') + ] for name in view_names: self.gui.create_and_add_view(name) self.qtbot.wait(250) @@ -110,8 +114,8 @@ def test_z1_close_reopen(self): # Recreate the controller on the model. self.__class__._controller = _template_controller( - self.__class__._tempdir, self.__class__._dataset.parent, - clear_cache=False) + self.__class__._tempdir, self.__class__._dataset.parent, clear_cache=False + ) self.__class__._create_gui() # Check that the data has been saved. @@ -164,9 +168,10 @@ def test_template_feature_view_split(self): return -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test plugins -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def plugins_dir(): """Path to the directory with the builtin plugins.""" @@ -190,7 +195,6 @@ def _make_plugin_test_case(plugin_name): """Generate a special test class with a plugin attached to the controller.""" class TemplateControllerPluginTests(MinimalControllerTests, unittest.TestCase): - @classmethod def _create_dataset(cls, tempdir): return _make_dataset(tempdir, param='dense', has_spike_attributes=False) @@ -216,4 +220,4 @@ def test_a2_minimal(self): # Dynamically define test classes for each builtin plugin. for plugin_name in plugin_names(): - globals()['TemplateController%sTests' % plugin_name] = _make_plugin_test_case(plugin_name) + globals()[f'TemplateController{plugin_name}Tests'] = _make_plugin_test_case(plugin_name) diff --git a/phy/apps/tests/test_base.py b/phy/apps/tests/test_base.py index 0319ca932..3f92af99d 100644 --- a/phy/apps/tests/test_base.py +++ b/phy/apps/tests/test_base.py @@ -1,45 +1,50 @@ -# -*- coding: utf-8 -*- - """Integration tests for the GUIs.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ -from itertools import cycle, islice import logging import os -from pathlib import Path import shutil import tempfile import unittest +from itertools import cycle, islice +from pathlib import Path import numpy as np -from pytestqt.plugin import QtBot - from phylib.io.mock import ( - artificial_features, artificial_traces, artificial_spike_clusters, artificial_spike_samples, - artificial_waveforms + artificial_features, + artificial_spike_clusters, + artificial_spike_samples, + artificial_traces, + artificial_waveforms, ) - -from phylib.utils import connect, unconnect, Bunch, reset, emit +from phylib.utils import Bunch, connect, emit, reset, unconnect +from pytestqt.plugin import QtBot from phy.cluster.views import ( - WaveformView, FeatureView, AmplitudeView, TraceView, TemplateView, + AmplitudeView, + FeatureView, + TemplateView, + TraceView, + WaveformView, ) from phy.gui.qt import Debouncer, create_app from phy.gui.widgets import Barrier from phy.plot.tests import mouse_click -from ..base import BaseController, WaveformMixin, FeatureMixin, TraceMixin, TemplateMixin + +from ..base import BaseController, FeatureMixin, TemplateMixin, TraceMixin, WaveformMixin logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Mock models and controller classes -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + -class MyModel(object): +class MyModel: seed = np.random.seed(0) n_channels = 8 n_spikes = 20000 @@ -54,7 +59,7 @@ class MyModel(object): metadata = {'group': {3: 'noise', 4: 'mua', 5: 'good'}} sample_rate = 10000 spike_attributes = {} - amplitudes = np.random.normal(size=n_spikes, loc=1, scale=.1) + amplitudes = np.random.normal(size=n_spikes, loc=1, scale=0.1) spike_clusters = artificial_spike_clusters(n_spikes, n_clusters) spike_templates = spike_clusters spike_samples = artificial_spike_samples(n_spikes) @@ -78,7 +83,8 @@ def get_template(self, template_id): nc = self.n_channels // 2 return Bunch( template=artificial_waveforms(1, self.n_samples_waveforms, nc)[0, ...], - channel_ids=self._get_some_channels(template_id, nc)) + channel_ids=self._get_some_channels(template_id, nc), + ) def save_spike_clusters(self, spike_clusters): pass @@ -99,51 +105,50 @@ def get_channel_amplitudes(self, cluster_id): class MyControllerW(WaveformMixin, MyController): """With waveform view.""" - pass class MyControllerF(FeatureMixin, MyController): """With feature view.""" - pass class MyControllerT(TraceMixin, MyController): """With trace view.""" - pass class MyControllerTmp(TemplateMixin, MyController): """With templates.""" - pass class MyControllerFull(TemplateMixin, WaveformMixin, FeatureMixin, TraceMixin, MyController): """With everything.""" - pass def _mock_controller(tempdir, cls): model = MyModel() return cls( - dir_path=tempdir, config_dir=tempdir / 'config', model=model, - clear_cache=True, enable_threading=False) + dir_path=tempdir, + config_dir=tempdir / 'config', + model=model, + clear_cache=True, + enable_threading=False, + ) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Base classes -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ -class MinimalControllerTests(object): +class MinimalControllerTests: # Methods to override - #-------------------------------------------------------------------------- + # -------------------------------------------------------------------------- @classmethod def get_controller(cls, tempdir): raise NotImplementedError() # Convenient properties - #-------------------------------------------------------------------------- + # -------------------------------------------------------------------------- @property def qtbot(self): @@ -186,7 +191,7 @@ def amplitude_view(self): return self.gui.list_views(AmplitudeView)[0] # Convenience methods - #-------------------------------------------------------------------------- + # -------------------------------------------------------------------------- def stop(self): # pragma: no cover """Used for debugging.""" @@ -230,10 +235,10 @@ def redo(self): def move(self, w): s = self.supervisor - getattr(s.actions, 'move_%s' % w)() + getattr(s.actions, f'move_{w}')() s.block() - def lasso(self, view, scale=1.): + def lasso(self, view, scale=1.0): w, h = view.canvas.get_size() w *= scale h *= scale @@ -243,7 +248,7 @@ def lasso(self, view, scale=1.): mouse_click(self.qtbot, view.canvas, (1, h - 1), modifiers=('Control',)) # Fixtures - #-------------------------------------------------------------------------- + # -------------------------------------------------------------------------- @classmethod def setUpClass(cls): @@ -284,9 +289,8 @@ def _close_gui(cls): class BaseControllerTests(MinimalControllerTests): - # Common test methods - #-------------------------------------------------------------------------- + # -------------------------------------------------------------------------- def test_common_01(self): """Select one cluster.""" @@ -355,7 +359,7 @@ def test_common_11(self): self.gui.view_actions.switch_raw_data_filter() -class GlobalViewsTests(object): +class GlobalViewsTests: def test_global_filter_1(self): self.next() cv = self.supervisor.cluster_view @@ -366,9 +370,10 @@ def test_global_sort_1(self): emit('table_sort', cv, self.cluster_ids[::-1]) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Mock test cases -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + class MockControllerTests(MinimalControllerTests, GlobalViewsTests, unittest.TestCase): """Empty mock controller.""" @@ -431,7 +436,7 @@ def feature_view(self): def test_feature_view_split(self): self.next() n = max(self.cluster_ids) - self.lasso(self.feature_view, .1) + self.lasso(self.feature_view, 0.1) self.split() # Split one cluster => Two new clusters should be selected after the split. self.assertEqual(self.selected[:2], [n + 1, n + 2]) @@ -501,6 +506,7 @@ def test_split_template_amplitude(self): class MockControllerFullTests(MinimalControllerTests, unittest.TestCase): """Mock controller with all views.""" + @classmethod def get_controller(cls, tempdir): return _mock_controller(tempdir, MyControllerFull) diff --git a/phy/apps/trace/__init__.py b/phy/apps/trace/__init__.py index 6a48da638..3000ebc51 100644 --- a/phy/apps/trace/__init__.py +++ b/phy/apps/trace/__init__.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- - """Trace GUI.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ from .gui import create_trace_gui, trace_gui # noqa diff --git a/phy/apps/trace/gui.py b/phy/apps/trace/gui.py index fbf9f1f1d..d5dd4b84e 100644 --- a/phy/apps/trace/gui.py +++ b/phy/apps/trace/gui.py @@ -1,11 +1,9 @@ -# -*- coding: utf-8 -*- - """Trace GUI.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import logging @@ -14,14 +12,15 @@ from phy.apps.template import get_template_params from phy.cluster.views.trace import TraceView, select_traces -from phy.gui import create_app, run_app, GUI +from phy.gui import GUI, create_app, run_app logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Trace GUI -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def create_trace_gui(obj, **kwargs): """Create the Trace GUI. @@ -50,8 +49,10 @@ def create_trace_gui(obj, **kwargs): return create_trace_gui(next(iter(params.pop('dat_path'))), **params) kwargs = { - k: v for k, v in kwargs.items() - if k in ('sample_rate', 'n_channels_dat', 'dtype', 'offset')} + k: v + for k, v in kwargs.items() + if k in ('sample_rate', 'n_channels_dat', 'dtype', 'offset') + } traces = get_ephys_reader(obj, **kwargs) create_app() @@ -59,9 +60,7 @@ def create_trace_gui(obj, **kwargs): gui.set_default_actions() def _get_traces(interval): - return Bunch( - data=select_traces( - traces, interval, sample_rate=traces.sample_rate)) + return Bunch(data=select_traces(traces, interval, sample_rate=traces.sample_rate)) # TODO: load channel information diff --git a/phy/apps/trace/tests/test_gui.py b/phy/apps/trace/tests/test_gui.py index f41e17937..f2bbde170 100644 --- a/phy/apps/trace/tests/test_gui.py +++ b/phy/apps/trace/tests/test_gui.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- - """Testing the Trace GUI.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import logging @@ -15,9 +13,10 @@ logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Tests -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_trace_gui_1(qtbot, template_path): # noqa gui = create_trace_gui(template_path) diff --git a/phy/cluster/__init__.py b/phy/cluster/__init__.py index 65e25a46e..b9aca068a 100644 --- a/phy/cluster/__init__.py +++ b/phy/cluster/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # flake8: noqa """Manual clustering facilities.""" diff --git a/phy/cluster/_history.py b/phy/cluster/_history.py index 472e73dec..c1ba5ae6c 100644 --- a/phy/cluster/_history.py +++ b/phy/cluster/_history.py @@ -1,17 +1,16 @@ -# -*- coding: utf-8 -*- - """History class for undo stack.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # History class -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + -class History(object): +class History: """Implement a history of actions with an undo stack.""" def __init__(self, base_item=None): @@ -84,7 +83,7 @@ def add(self, item): """Add an item in the history.""" self._check_index() # Possibly truncate the history up to the current point. - self._history = self._history[:self._index + 1] + self._history = self._history[: self._index + 1] # Append the item self._history.append(item) # Increment the index. @@ -130,7 +129,7 @@ class GlobalHistory(History): """Merge several controllers with different undo stacks.""" def __init__(self, process_ups=None): - super(GlobalHistory, self).__init__(()) + super().__init__(()) self.process_ups = process_ups def action(self, *controllers): @@ -152,8 +151,7 @@ def undo(self): if controllers is None: ups = () else: - ups = tuple([controller.undo() - for controller in controllers]) + ups = tuple([controller.undo() for controller in controllers]) if self.process_ups is not None: return self.process_ups(ups) else: @@ -169,8 +167,7 @@ def redo(self): if controllers is None: ups = () else: - ups = tuple([controller.redo() for - controller in controllers]) + ups = tuple([controller.redo() for controller in controllers]) if self.process_ups is not None: return self.process_ups(ups) else: diff --git a/phy/cluster/_utils.py b/phy/cluster/_utils.py index aa36c62fd..390f35cb0 100644 --- a/phy/cluster/_utils.py +++ b/phy/cluster/_utils.py @@ -1,25 +1,24 @@ -# -*- coding: utf-8 -*- - """Clustering utility functions.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ - -import numpy as np +# ------------------------------------------------------------------------------ -from copy import deepcopy import logging +from copy import deepcopy -from ._history import History +import numpy as np from phylib.utils import Bunch, _as_list, _is_list, emit, silent +from ._history import History + logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Utility functions -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _update_cluster_selection(clusters, up): clusters = list(clusters) @@ -30,7 +29,7 @@ def _update_cluster_selection(clusters, up): def _join(clusters): - return '[{}]'.format(', '.join(map(str, clusters))) + return f'[{", ".join(map(str, clusters))}]' def create_cluster_meta(cluster_groups): @@ -45,9 +44,10 @@ def create_cluster_meta(cluster_groups): return meta -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # UpdateInfo class -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + class UpdateInfo(Bunch): """Object created every time the dataset is modified via a clustering or cluster metadata @@ -79,47 +79,48 @@ class UpdateInfo(Bunch): when redoing the undone action. """ + def __init__(self, **kwargs): - d = dict( - description='', - history=None, - spike_ids=[], - added=[], - deleted=[], - descendants=[], - metadata_changed=[], - metadata_value=None, - undo_state=None, - ) + d = { + 'description': '', + 'history': None, + 'spike_ids': [], + 'added': [], + 'deleted': [], + 'descendants': [], + 'metadata_changed': [], + 'metadata_value': None, + 'undo_state': None, + } d.update(kwargs) - super(UpdateInfo, self).__init__(d) + super().__init__(d) # NOTE: we have to ensure we only use native types and not NumPy arrays so that # the history stack works correctly. assert all(not isinstance(v, np.ndarray) for v in self.values()) def __repr__(self): desc = self.description - h = ' ({})'.format(self.history) if self.history else '' + h = f' ({self.history})' if self.history else '' if not desc: return '' elif desc in ('merge', 'assign'): a, d = _join(self.added), _join(self.deleted) - return '<{desc}{h} {d} => {a}>'.format( - desc=desc, a=a, d=d, h=h) + return f'<{desc}{h} {d} => {a}>' elif desc.startswith('metadata'): c = _join(self.metadata_changed) m = self.metadata_value - return '<{desc}{h} {c} => {m}>'.format( - desc=desc, c=c, m=m, h=h) + return f'<{desc}{h} {c} => {m}>' return '' -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # ClusterMetadataUpdater class -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + -class ClusterMeta(object): +class ClusterMeta: """Handle cluster metadata changes.""" + def __init__(self): self._fields = {} self._reset_data() @@ -147,7 +148,7 @@ def func(cluster): def from_dict(self, dic): """Import data from a `{cluster_id: {field: value}}` dictionary.""" - #self._reset_data() + # self._reset_data() # Do not raise events here. with silent(): for cluster, vals in dic.items(): @@ -192,10 +193,11 @@ def set(self, field, clusters, value, add_to_stack=True): self._data[cluster] = {} self._data[cluster][field] = value - up = UpdateInfo(description='metadata_' + field, - metadata_changed=clusters, - metadata_value=value, - ) + up = UpdateInfo( + description=f'metadata_{field}', + metadata_changed=clusters, + metadata_value=value, + ) undo_state = emit('request_undo_state', self, up) if add_to_stack: @@ -229,7 +231,7 @@ def set_from_descendants(self, descendants, largest_old_cluster=None): # This maps old cluster ids to their values. old_values = {old: self.get(field, old) for old, _ in descendants} # This is the set of new clusters. - new_clusters = set(new for _, new in descendants) + new_clusters = {new for _, new in descendants} # This is the set of old non-default values. old_values_set = set(old_values.values()) if default in old_values_set: @@ -305,8 +307,10 @@ def redo(self): # Property cycle # ----------------------------------------------------------------------------- -class RotatingProperty(object): + +class RotatingProperty: """A key-value property of a view that can switch between several predefined values.""" + def __init__(self): self._choices = {} self._current = None diff --git a/phy/cluster/clustering.py b/phy/cluster/clustering.py index 872a49ba1..c1cc92356 100644 --- a/phy/cluster/clustering.py +++ b/phy/cluster/clustering.py @@ -1,27 +1,26 @@ -# -*- coding: utf-8 -*- - """Clustering structure.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import logging import numpy as np - +from phylib.io.array import _spikes_in_clusters, _spikes_per_cluster, _unique from phylib.utils._types import _as_array, _is_array_like -from phylib.io.array import _unique, _spikes_in_clusters, _spikes_per_cluster -from ._utils import UpdateInfo -from ._history import History from phylib.utils.event import emit +from ._history import History +from ._utils import UpdateInfo + logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Clustering class -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _extend_spikes(spike_ids, spike_clusters): """Return all spikes belonging to the clusters containing the specified @@ -58,7 +57,7 @@ def _extend_assignment(spike_ids, old_spike_clusters, spike_clusters_rel, new_cl assert spike_clusters_rel.min() >= 0 # We renumber the new cluster indices. - new_spike_clusters = (spike_clusters_rel + (new_cluster_id - spike_clusters_rel.min())) + new_spike_clusters = spike_clusters_rel + (new_cluster_id - spike_clusters_rel.min()) # We find the spikes belonging to modified clusters. extended_spike_ids = _extend_spikes(spike_ids, old_spike_clusters) @@ -71,11 +70,12 @@ def _extend_assignment(spike_ids, old_spike_clusters, spike_clusters_rel, new_cl _, extended_spike_clusters = np.unique(extended_spike_clusters, return_inverse=True) # Generate new cluster numbers. k = new_spike_clusters.max() + 1 - extended_spike_clusters += (k - extended_spike_clusters.min()) + extended_spike_clusters += k - extended_spike_clusters.min() # Finally, we concatenate spike_ids and extended_spike_ids. return _concatenate_spike_clusters( - (spike_ids, new_spike_clusters), (extended_spike_ids, extended_spike_clusters)) + (spike_ids, new_spike_clusters), (extended_spike_ids, extended_spike_clusters) + ) def _assign_update_info(spike_ids, old_spike_clusters, new_spike_clusters): @@ -95,7 +95,7 @@ def _assign_update_info(spike_ids, old_spike_clusters, new_spike_clusters): return update_info -class Clustering(object): +class Clustering: """Handle cluster changes in a set of spikes. Constructor @@ -139,9 +139,8 @@ class Clustering(object): """ - def __init__(self, spike_clusters, new_cluster_id=None, - spikes_per_cluster=None): - super(Clustering, self).__init__() + def __init__(self, spike_clusters, new_cluster_id=None, spikes_per_cluster=None): + super().__init__() self._undo_stack = History(base_item=(None, None, None)) # Spike -> cluster mapping. self._spike_clusters = _as_array(spike_clusters) @@ -217,7 +216,7 @@ def spikes_in_clusters(self, clusters): return _spikes_in_clusters(self.spike_clusters, clusters) # Actions - #-------------------------------------------------------------------------- + # -------------------------------------------------------------------------- def _update_cluster_ids(self, to_remove=None, to_add=None): # Update the list of non-empty cluster ids. @@ -234,7 +233,7 @@ def _update_cluster_ids(self, to_remove=None, to_add=None): # spikes_per_cluster array. coherent = np.all(np.isin(self._cluster_ids, sorted(self._spikes_per_cluster))) if not coherent: - logger.debug("Recompute spikes_per_cluster manually: this might take a while.") + logger.debug('Recompute spikes_per_cluster manually: this might take a while.') sc = self._spike_clusters self._spikes_per_cluster = _spikes_per_cluster(sc) @@ -277,7 +276,6 @@ def _do_assign(self, spike_ids, new_spike_clusters): return up def _do_merge(self, spike_ids, cluster_ids, to): - # Create the UpdateInfo instance here. descendants = [(cluster, to) for cluster in cluster_ids] largest_old_cluster = np.bincount(self.spike_clusters[spike_ids]).argmax() @@ -320,18 +318,19 @@ def merge(self, cluster_ids, to=None): """ if not _is_array_like(cluster_ids): - raise ValueError("The first argument should be a list or an array.") + raise ValueError('The first argument should be a list or an array.') cluster_ids = sorted(cluster_ids) if not set(cluster_ids) <= set(self.cluster_ids): - raise ValueError("Some clusters do not exist.") + raise ValueError('Some clusters do not exist.') # Find the new cluster number. if to is None: to = self.new_cluster_id() if to < self.new_cluster_id(): raise ValueError( - "The new cluster numbers should be higher than {0}.".format(self.new_cluster_id())) + f'The new cluster numbers should be higher than {self.new_cluster_id()}.' + ) # NOTE: we could have called self.assign() here, but we don't. # We circumvent self.assign() for performance reasons. @@ -413,7 +412,8 @@ def assign(self, spike_ids, spike_clusters_rel=0): # belong to clusters affected by the operation, will be assigned # to brand new clusters. spike_ids, cluster_ids = _extend_assignment( - spike_ids, self._spike_clusters, spike_clusters_rel, self.new_cluster_id()) + spike_ids, self._spike_clusters, spike_clusters_rel, self.new_cluster_id() + ) up = self._do_assign(spike_ids, cluster_ids) undo_state = emit('request_undo_state', self, up) diff --git a/phy/cluster/supervisor.py b/phy/cluster/supervisor.py index 759c823c8..6f9f0415c 100644 --- a/phy/cluster/supervisor.py +++ b/phy/cluster/supervisor.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """Manual clustering GUI component.""" @@ -7,21 +5,21 @@ # Imports # ----------------------------------------------------------------------------- -from functools import partial import inspect import logging +from functools import partial import numpy as np +from phylib.utils import Bunch, connect, emit, unconnect + +from phy.gui.actions import Actions +from phy.gui.qt import _block, _wait, set_busy +from phy.gui.widgets import Barrier, HTMLWidget, Table, _uniq from ._history import GlobalHistory from ._utils import create_cluster_meta from .clustering import Clustering -from phylib.utils import Bunch, emit, connect, unconnect -from phy.gui.actions import Actions -from phy.gui.qt import _block, set_busy, _wait -from phy.gui.widgets import Table, HTMLWidget, _uniq, Barrier - logger = logging.getLogger(__name__) @@ -29,6 +27,7 @@ # Utility functions # ----------------------------------------------------------------------------- + def _process_ups(ups): # pragma: no cover """This function processes the UpdateInfo instances of the two undo stacks (clustering and cluster metadata) and concatenates them @@ -46,7 +45,7 @@ def _process_ups(ups): # pragma: no cover def _ensure_all_ints(l): - if (l is None or l == []): + if l is None or l == []: return for i in range(len(l)): l[i] = int(l[i]) @@ -56,7 +55,8 @@ def _ensure_all_ints(l): # Tasks # ----------------------------------------------------------------------------- -class TaskLogger(object): + +class TaskLogger: """Internal object that gandles all clustering actions and the automatic actions that should follow as part of the "wizard".""" @@ -77,7 +77,14 @@ def enqueue(self, sender, name, *args, output=None, **kwargs): """Enqueue an action, which has a sender, a function name, a list of arguments, and an optional output.""" logger.log( - 5, "Enqueue %s %s %s %s (%s)", sender.__class__.__name__, name, args, kwargs, output) + 5, + 'Enqueue %s %s %s %s (%s)', + sender.__class__.__name__, + name, + args, + kwargs, + output, + ) self._queue.append((sender, name, args, kwargs)) def dequeue(self): @@ -101,7 +108,9 @@ def _callback(self, task, output): def _eval(self, task): """Evaluate a task and call a callback function.""" sender, name, args, kwargs = task - logger.log(5, "Calling %s.%s(%s)", sender.__class__.__name__, name, args, kwargs) + logger.log( + 5, 'Calling %s.%s(%s)', sender.__class__.__name__, name, args, kwargs + ) f = getattr(sender, name) callback = partial(self._callback, task) argspec = inspect.getfullargspec(f) @@ -112,6 +121,7 @@ def _eval(self, task): # HACK: use on_cluster event instead of callback. def _cluster_callback(tsender, up): self._callback(task, up) + connect(_cluster_callback, event='cluster', sender=self.supervisor) f(*args, **kwargs) unconnect(_cluster_callback) @@ -129,8 +139,8 @@ def process(self): def enqueue_after(self, task, output): """Enqueue tasks after a given action.""" sender, name, args, kwargs = task - f = lambda *args, **kwargs: logger.log(5, "No method _after_%s", name) - getattr(self, '_after_%s' % name, f)(task, output) + f = lambda *args, **kwargs: logger.log(5, 'No method _after_%s', name) + getattr(self, f'_after_{name}', f)(task, output) def _after_merge(self, task, output): """Tasks that should follow a merge.""" @@ -180,7 +190,9 @@ def _after_move(self, task, output): def _after_undo(self, task, output): """Task that should follow an undo.""" - last_action = self.last_task(name_not_in=('select', 'next', 'previous', 'undo', 'redo')) + last_action = self.last_task( + name_not_in=('select', 'next', 'previous', 'undo', 'redo') + ) self._select_state(self.last_state(last_action)) def _after_redo(self, task, output): @@ -192,8 +204,7 @@ def _after_redo(self, task, output): def _select_state(self, state): """Enqueue select actions when a state (selected clusters and similar clusters) is set.""" cluster_ids, next_cluster, similar, next_similar = state - self.enqueue( - self.cluster_view, 'select', cluster_ids, update_views=False if similar else True) + self.enqueue(self.cluster_view, 'select', cluster_ids, update_views=not similar) if similar: self.enqueue(self.similarity_view, 'select', similar) @@ -203,7 +214,14 @@ def _log(self, task, output): assert sender assert name logger.log( - 5, "Log %s %s %s %s (%s)", sender.__class__.__name__, name, args, kwargs, output) + 5, + 'Log %s %s %s %s (%s)', + sender.__class__.__name__, + name, + args, + kwargs, + output, + ) args = [a.tolist() if isinstance(a, np.ndarray) else a for a in args] task = (sender, name, args, kwargs, output) # Avoid successive duplicates (even if sender is different). @@ -216,8 +234,10 @@ def log(self, sender, name, *args, output=None, **kwargs): def last_task(self, name=None, name_not_in=()): """Return the last executed task.""" - for (sender, name_, args, kwargs, output) in reversed(self._history): - if (name and name_ == name) or (name_not_in and name_ and name_ not in name_not_in): + for sender, name_, args, kwargs, output in reversed(self._history): + if (name and name_ == name) or ( + name_not_in and name_ and name_ not in name_not_in + ): assert name_ return (sender, name_, args, kwargs, output) @@ -230,23 +250,31 @@ def last_state(self, task=None): if task: i = self._history.index(task) h = self._history[:i] - for (sender, name, args, kwargs, output) in reversed(h): + for sender, name, args, kwargs, output in reversed(h): # Last selection is cluster view selection: return the state. - if (sender == self.similarity_view and similarity_state == (None, None) and - name in ('select', 'next', 'previous')): - similarity_state = (output['selected'], output['next']) if output else (None, None) - if (sender == self.cluster_view and - cluster_state == (None, None) and - name in ('select', 'next', 'previous')): - cluster_state = (output['selected'], output['next']) if output else (None, None) + if ( + sender == self.similarity_view + and similarity_state == (None, None) + and name in ('select', 'next', 'previous') + ): + similarity_state = ( + (output['selected'], output['next']) if output else (None, None) + ) + if ( + sender == self.cluster_view + and cluster_state == (None, None) + and name in ('select', 'next', 'previous') + ): + cluster_state = ( + (output['selected'], output['next']) if output else (None, None) + ) return (*cluster_state, *similarity_state) def show_history(self): """Show the history stack.""" - print("=== History ===") + print('=== History ===') for sender, name, args, kwargs, output in self._history: - print( - '{: <24} {: <8}'.format(sender.__class__.__name__, name), *args, output, kwargs) + print(f'{sender.__class__.__name__: <24} {name: <8}', *args, output, kwargs) def has_finished(self): """Return whether the queue has finished being processed.""" @@ -257,7 +285,7 @@ def has_finished(self): # Cluster view and similarity view # ----------------------------------------------------------------------------- -_CLUSTER_VIEW_STYLES = ''' +_CLUSTER_VIEW_STYLES = """ table tr[data-group='good'] { color: #86D16D; } @@ -269,7 +297,7 @@ def has_finished(self): table tr[data-group='noise'] { color: #777; } -''' +""" class ClusterView(Table): @@ -296,13 +324,14 @@ class ClusterView(Table): def __init__(self, *args, data=None, columns=(), sort=None): # NOTE: debounce select events. HTMLWidget.__init__( - self, *args, title=self.__class__.__name__, debounce_events=('select',)) + self, *args, title=self.__class__.__name__, debounce_events=('select',) + ) self._set_styles() self._reset_table(data=data, columns=columns, sort=sort) def _reset_table(self, data=None, columns=(), sort=None): """Recreate the table with specified columns, data, and sort.""" - emit(self._view_name + '_init', self) + emit(f'{self._view_name}_init', self) # Ensure 'id' is the first column. if 'id' in columns: columns.remove('id') @@ -370,7 +399,7 @@ class SimilarityView(ClusterView): def set_selected_index_offset(self, n): """Set the index of the selected cluster, used for correct coloring in the similarity view.""" - self.eval_js('table._setSelectedIndexOffset(%d);' % n) + self.eval_js(f'table._setSelectedIndexOffset({n});') def reset(self, cluster_ids): """Recreate the similarity view, given the selected clusters in the cluster view.""" @@ -380,7 +409,8 @@ def reset(self, cluster_ids): # Clear the table. if similar: self.remove_all_and_add( - [cl for cl in similar[0] if cl['id'] not in cluster_ids]) + [cl for cl in similar[0] if cl['id'] not in cluster_ids] + ) else: # pragma: no cover self.remove_all() return similar @@ -390,32 +420,28 @@ def reset(self, cluster_ids): # ActionCreator # ----------------------------------------------------------------------------- -class ActionCreator(object): + +class ActionCreator: """Companion class to the Supervisor that manages the related GUI actions.""" default_shortcuts = { # Clustering. 'merge': 'g', 'split': 'k', - 'label': 'l', - # Move. 'move_best_to_noise': 'alt+n', 'move_best_to_mua': 'alt+m', 'move_best_to_good': 'alt+g', 'move_best_to_unsorted': 'alt+u', - 'move_similar_to_noise': 'ctrl+n', 'move_similar_to_mua': 'ctrl+m', 'move_similar_to_good': 'ctrl+g', 'move_similar_to_unsorted': 'ctrl+u', - 'move_all_to_noise': 'ctrl+alt+n', 'move_all_to_mua': 'ctrl+alt+m', 'move_all_to_good': 'ctrl+alt+g', 'move_all_to_unsorted': 'ctrl+alt+u', - # Wizard. 'first': 'home', 'last': 'end', @@ -425,11 +451,9 @@ class ActionCreator(object): 'unselect_similar': 'backspace', 'next_best': 'down', 'previous_best': 'up', - # Misc. 'undo': 'ctrl+z', 'redo': ('ctrl+shift+z', 'ctrl+y'), - 'clear_filter': 'esc', } @@ -454,9 +478,9 @@ def add(self, which, name, **kwargs): emit_fun = partial(emit, 'action', self, method_name, *method_args) f = getattr(self.supervisor, method_name, None) docstring = inspect.getdoc(f) if f else name - if not kwargs.get('docstring', None): + if not kwargs.get('docstring'): kwargs['docstring'] = docstring - getattr(self, '%s_actions' % which).add(emit_fun, name=name, **kwargs) + getattr(self, f'{which}_actions').add(emit_fun, name=name, **kwargs) def attach(self, gui): """Attach the GUI and create the menus.""" @@ -464,11 +488,21 @@ def attach(self, gui): ds = self.default_shortcuts dsp = self.default_snippets self.edit_actions = Actions( - gui, name='Edit', menu='&Edit', insert_menu_before='&View', - default_shortcuts=ds, default_snippets=dsp) + gui, + name='Edit', + menu='&Edit', + insert_menu_before='&View', + default_shortcuts=ds, + default_snippets=dsp, + ) self.select_actions = Actions( - gui, name='Select', menu='Sele&ct', insert_menu_before='&View', - default_shortcuts=ds, default_snippets=dsp) + gui, + name='Select', + menu='Sele&ct', + insert_menu_before='&View', + default_shortcuts=ds, + default_snippets=dsp, + ) # Create the actions. self._create_edit_actions() @@ -491,11 +525,13 @@ def _create_edit_actions(self): for which in ('best', 'similar', 'all'): for group in ('noise', 'mua', 'good', 'unsorted'): self.add( - w, 'move_%s_to_%s' % (which, group), + w, + f'move_{which}_to_{group}', method_name='move', method_args=(group, which), - submenu='Move %s to' % which, - docstring='Move %s to %s.' % (which, group)) + submenu=f'Move {which} to', + docstring=f'Move {which} to {group}.', + ) self.edit_actions.separator() # Label. @@ -518,9 +554,14 @@ def _create_select_actions(self): # Sort by: for column in getattr(self.supervisor, 'columns', ()): self.add( - w, 'sort_by_%s' % column.lower(), method_name='sort', method_args=(column,), - docstring='Sort by %s' % column, - submenu='Sort by', alias='s%s' % column.replace('_', '')[:2]) + w, + f'sort_by_{column.lower()}', + method_name='sort', + method_args=(column,), + docstring=f'Sort by {column}', + submenu='Sort by', + alias=f's{column.replace("_", "")[:2]}', + ) self.select_actions.separator() self.add(w, 'first') @@ -556,11 +597,12 @@ def _create_toolbar(self, gui): # Clustering GUI component # ----------------------------------------------------------------------------- + def _is_group_masked(group): return group in ('noise', 'mua') -class Supervisor(object): +class Supervisor: """Component that brings manual clustering facilities to a GUI: * `Clustering` instance: merge, split, undo, redo. @@ -608,9 +650,17 @@ class Supervisor(object): """ def __init__( - self, spike_clusters=None, cluster_groups=None, cluster_metrics=None, - cluster_labels=None, similarity=None, new_cluster_id=None, sort=None, context=None): - super(Supervisor, self).__init__() + self, + spike_clusters=None, + cluster_groups=None, + cluster_metrics=None, + cluster_labels=None, + similarity=None, + new_cluster_id=None, + sort=None, + context=None, + ): + super().__init__() self.context = context self.similarity = similarity # function cluster => [(cl, sim), ...] self.actions = None # will be set when attaching the GUI @@ -629,14 +679,17 @@ def __init__( self.columns = ['id'] # n_spikes comes from cluster_metrics self.columns += list(self.cluster_metrics.keys()) self.columns += [ - label for label in self.cluster_labels.keys() - if label not in self.columns + ['group']] + label + for label in self.cluster_labels.keys() + if label not in self.columns + ['group'] + ] # Create Clustering and ClusterMeta. # Load the cached spikes_per_cluster array. spc = context.load('spikes_per_cluster') if context else None self.clustering = Clustering( - spike_clusters, spikes_per_cluster=spc, new_cluster_id=new_cluster_id) + spike_clusters, spikes_per_cluster=spc, new_cluster_id=new_cluster_id + ) # Cache the spikes_per_cluster array. self._save_spikes_per_cluster() @@ -670,7 +723,8 @@ def on_cluster(sender, up): # the largest cluster wins and its value is set to its descendants. if up.added: self.cluster_meta.set_from_descendants( - up.descendants, largest_old_cluster=up.largest_old_cluster) + up.descendants, largest_old_cluster=up.largest_old_cluster + ) emit('cluster', self, up) @connect(sender=self.cluster_meta) # noqa @@ -688,29 +742,36 @@ def _save_spikes_per_cluster(self): """Cache on the disk the dictionary with the spikes belonging to each cluster.""" if not self.context: return - self.context.save('spikes_per_cluster', self.clustering.spikes_per_cluster, kind='pickle') + self.context.save( + 'spikes_per_cluster', self.clustering.spikes_per_cluster, kind='pickle' + ) def _log_action(self, sender, up): """Log the clustering action (merge, split).""" if sender != self.clustering: return if up.history: - logger.info(up.history.title() + " cluster assign.") + logger.info(f'{up.history.title()} cluster assign.') elif up.description == 'merge': - logger.info("Merge clusters %s to %s.", ', '.join(map(str, up.deleted)), up.added[0]) + logger.info( + 'Merge clusters %s to %s.', ', '.join(map(str, up.deleted)), up.added[0] + ) else: - logger.info("Assigned %s spikes.", len(up.spike_ids)) + logger.info('Assigned %s spikes.', len(up.spike_ids)) def _log_action_meta(self, sender, up): """Log the cluster meta action (move, label).""" if sender != self.cluster_meta: return if up.history: - logger.info(up.history.title() + " move.") + logger.info(f'{up.history.title()} move.') else: logger.info( - "Change %s for clusters %s to %s.", up.description, - ', '.join(map(str, up.metadata_changed)), up.metadata_value) + 'Change %s for clusters %s to %s.', + up.description, + ', '.join(map(str, up.metadata_changed)), + up.metadata_value, + ) # Skip cluster metadata other than groups. if up.description != 'metadata_group': @@ -721,8 +782,8 @@ def _save_new_cluster_id(self, sender, up): easier cache consistency.""" new_cluster_id = self.clustering.new_cluster_id() if self.context: - logger.log(5, "Save the new cluster id: %d.", new_cluster_id) - self.context.save('new_cluster_id', dict(new_cluster_id=new_cluster_id)) + logger.log(5, 'Save the new cluster id: %d.', new_cluster_id) + self.context.save('new_cluster_id', {'new_cluster_id': new_cluster_id}) def _save_gui_state(self, gui): """Save the GUI state with the cluster view and similarity view.""" @@ -738,8 +799,10 @@ def _get_similar_clusters(self, sender, cluster_id): # Only keep existing clusters. clusters_set = set(self.clustering.cluster_ids) data = [ - dict(similarity='%.3f' % s, **self.get_cluster_info(c)) - for c, s in sim if c in clusters_set] + dict(similarity=f'{s:.3f}', **self.get_cluster_info(c)) + for c, s in sim + if c in clusters_set + ] return data def get_cluster_info(self, cluster_id, exclude=()): @@ -752,7 +815,7 @@ def get_cluster_info(self, cluster_id, exclude=()): for key in self.cluster_meta.fields: # includes group out[key] = self.cluster_meta.get(key, cluster_id) - out['is_masked'] = _is_group_masked(out.get('group', None)) + out['is_masked'] = _is_group_masked(out.get('group')) return {k: v for k, v in out.items() if k not in exclude} def _create_views(self, gui=None, sort=None): @@ -762,16 +825,20 @@ def _create_views(self, gui=None, sort=None): # Create the cluster view. self.cluster_view = ClusterView( - gui, data=self.cluster_info, columns=self.columns, sort=sort) + gui, data=self.cluster_info, columns=self.columns, sort=sort + ) # Update the action flow and similarity view when selection changes. connect(self._clusters_selected, event='select', sender=self.cluster_view) # Create the similarity view. self.similarity_view = SimilarityView( - gui, columns=self.columns + ['similarity'], sort=('similarity', 'desc')) + gui, columns=self.columns + ['similarity'], sort=('similarity', 'desc') + ) connect( - self._get_similar_clusters, event='request_similar_clusters', - sender=self.similarity_view) + self._get_similar_clusters, + event='request_similar_clusters', + sender=self.similarity_view, + ) connect(self._similar_selected, event='select', sender=self.similarity_view) # Change the state after every clustering action, according to the action flow. @@ -779,26 +846,27 @@ def _create_views(self, gui=None, sort=None): def _reset_cluster_view(self): """Recreate the cluster view.""" - logger.debug("Reset the cluster view.") + logger.debug('Reset the cluster view.') self.cluster_view._reset_table( - data=self.cluster_info, columns=self.columns, sort=self._sort) + data=self.cluster_info, columns=self.columns, sort=self._sort + ) def _clusters_added(self, cluster_ids): """Update the cluster and similarity views when new clusters are created.""" - logger.log(5, "Clusters added: %s", cluster_ids) + logger.log(5, 'Clusters added: %s', cluster_ids) data = [self.get_cluster_info(cluster_id) for cluster_id in cluster_ids] self.cluster_view.add(data) self.similarity_view.add(data) def _clusters_removed(self, cluster_ids): """Update the cluster and similarity views when clusters are removed.""" - logger.log(5, "Clusters removed: %s", cluster_ids) + logger.log(5, 'Clusters removed: %s', cluster_ids) self.cluster_view.remove(cluster_ids) self.similarity_view.remove(cluster_ids) def _cluster_metadata_changed(self, field, cluster_ids, value): """Update the cluster and similarity views when clusters metadata is updated.""" - logger.log(5, "%s changed for %s to %s", field, cluster_ids, value) + logger.log(5, '%s changed for %s to %s', field, cluster_ids, value) data = [{'id': cluster_id, field: value} for cluster_id in cluster_ids] for _ in data: _['is_masked'] = _is_group_masked(_.get('group', None)) @@ -814,7 +882,7 @@ def _clusters_selected(self, sender, obj, **kwargs): cluster_ids = obj['selected'] next_cluster = obj['next'] kwargs = obj.get('kwargs', {}) - logger.debug("Clusters selected: %s (%s)", cluster_ids, next_cluster) + logger.debug('Clusters selected: %s (%s)', cluster_ids, next_cluster) self.task_logger.log(self.cluster_view, 'select', cluster_ids, output=obj) # Update the similarity view when the cluster view selection changes. self.similarity_view.reset(cluster_ids) @@ -826,7 +894,9 @@ def _clusters_selected(self, sender, obj, **kwargs): emit('select', self, self.selected, **kwargs) if cluster_ids: self.cluster_view.scroll_to(cluster_ids[-1]) - self.cluster_view.dock.set_status('clusters: %s' % ', '.join(map(str, cluster_ids))) + self.cluster_view.dock.set_status( + f'clusters: {", ".join(map(str, cluster_ids))}' + ) def _similar_selected(self, sender, obj): """When clusters are selected in the similarity view, register the action in the history @@ -836,23 +906,25 @@ def _similar_selected(self, sender, obj): similar = obj['selected'] next_similar = obj['next'] kwargs = obj.get('kwargs', {}) - logger.debug("Similar clusters selected: %s (%s)", similar, next_similar) + logger.debug('Similar clusters selected: %s (%s)', similar, next_similar) self.task_logger.log(self.similarity_view, 'select', similar, output=obj) emit('select', self, self.selected, **kwargs) if similar: self.similarity_view.scroll_to(similar[-1]) - self.similarity_view.dock.set_status('similar clusters: %s' % ', '.join(map(str, similar))) + self.similarity_view.dock.set_status( + f'similar clusters: {", ".join(map(str, similar))}' + ) def _on_action(self, sender, name, *args): """Called when an action is triggered: enqueue and process the task.""" assert sender == self.action_creator # The GUI should not be busy when calling a new action. if 'select' not in name and self._is_busy: - logger.log(5, "The GUI is busy, waiting before calling the action.") + logger.log(5, 'The GUI is busy, waiting before calling the action.') try: _block(lambda: not self._is_busy) except Exception: - logger.warning("The GUI is busy, could not execute `%s`.", name) + logger.warning('The GUI is busy, could not execute `%s`.', name) return # Enqueue the requested action. self.task_logger.enqueue(self, name, *args) @@ -867,7 +939,10 @@ def _after_action(self, sender, up): self._clusters_added(up.added) self._clusters_removed(up.deleted) self._cluster_metadata_changed( - up.description.replace('metadata_', ''), up.metadata_changed, up.metadata_value) + up.description.replace('metadata_', ''), + up.metadata_changed, + up.metadata_value, + ) # After the action has finished, we process the pending actions, # like selection of new clusters in the tables. self.task_logger.process() @@ -878,7 +953,7 @@ def _set_busy(self, busy): return self._is_busy = busy # Set the busy cursor. - logger.log(5, "GUI is %sbusy" % ('' if busy else 'not ')) + logger.log(5, f'GUI is {"" if busy else "not "}busy') set_busy(busy) # Let the cluster views know that the GUI is busy. self.cluster_view.set_busy(busy) @@ -898,7 +973,7 @@ def select(self, *cluster_ids, callback=None): if cluster_ids and isinstance(cluster_ids[0], (tuple, list)): cluster_ids = list(cluster_ids[0]) + list(cluster_ids[1:]) # Remove non-existing clusters from the selection. - #cluster_ids = self._keep_existing_clusters(cluster_ids) + # cluster_ids = self._keep_existing_clusters(cluster_ids) # Update the cluster view selection. self.cluster_view.select(cluster_ids, callback=callback) @@ -922,7 +997,10 @@ def clear_filter(self): @property def cluster_info(self): """The cluster view table as a list of per-cluster dictionaries.""" - return [self.get_cluster_info(cluster_id) for cluster_id in self.clustering.cluster_ids] + return [ + self.get_cluster_info(cluster_id) + for cluster_id in self.clustering.cluster_ids + ] @property def shown_cluster_ids(self): @@ -948,7 +1026,8 @@ def attach(self, gui): # Create the cluster view and similarity view. self._create_views( - gui=gui, sort=gui.state.get('ClusterView', {}).get('current_sort', None)) + gui=gui, sort=gui.state.get('ClusterView', {}).get('current_sort', None) + ) # Create the TaskLogger. self.task_logger = TaskLogger( @@ -1038,11 +1117,9 @@ def split(self, spike_ids=None, spike_clusters_rel=0): assert spike_ids.dtype == np.int64 assert spike_ids.ndim == 1 if len(spike_ids) == 0: - logger.warning( - """No spikes selected, cannot split.""") + logger.warning("""No spikes selected, cannot split.""") return - out = self.clustering.split( - spike_ids, spike_clusters_rel=spike_clusters_rel) + out = self.clustering.split(spike_ids, spike_clusters_rel=spike_clusters_rel) self._global_history.action(self.clustering) return out @@ -1056,8 +1133,7 @@ def fields(self): def get_labels(self, field): """Return the labels of all clusters, for a given label name.""" - return {c: self.cluster_meta.get(field, c) - for c in self.clustering.cluster_ids} + return {c: self.cluster_meta.get(field, c) for c in self.clustering.cluster_ids} def label(self, name, value, cluster_ids=None): """Assign a label to some clusters.""" @@ -1071,7 +1147,7 @@ def label(self, name, value, cluster_ids=None): self._global_history.action(self.cluster_meta) # Add column if needed. if name != 'group' and name not in self.columns: - logger.debug("Add column %s.", name) + logger.debug('Add column %s.', name) self.columns.append(name) self._reset_cluster_view() @@ -1088,7 +1164,7 @@ def move(self, group, which): if not which: return _ensure_all_ints(which) - logger.debug("Move %s to %s.", which, group) + logger.debug('Move %s to %s.', which, group) group = 'unsorted' if group is None else group self.label('group', group, cluster_ids=which) @@ -1108,19 +1184,27 @@ def next_best(self, callback=None): def previous_best(self, callback=None): """Select the previous best cluster in the cluster view.""" - self.cluster_view.previous(callback=callback or partial(emit, 'wizard_done', self)) + self.cluster_view.previous( + callback=callback or partial(emit, 'wizard_done', self) + ) def next(self, callback=None): """Select the next cluster in the similarity view.""" state = self.task_logger.last_state() if not state or not state[0]: - self.cluster_view.first(callback=callback or partial(emit, 'wizard_done', self)) + self.cluster_view.first( + callback=callback or partial(emit, 'wizard_done', self) + ) else: - self.similarity_view.next(callback=callback or partial(emit, 'wizard_done', self)) + self.similarity_view.next( + callback=callback or partial(emit, 'wizard_done', self) + ) def previous(self, callback=None): """Select the previous cluster in the similarity view.""" - self.similarity_view.previous(callback=callback or partial(emit, 'wizard_done', self)) + self.similarity_view.previous( + callback=callback or partial(emit, 'wizard_done', self) + ) def unselect_similar(self, callback=None): """Select only the clusters in the cluster view.""" @@ -1139,7 +1223,11 @@ def last(self, callback=None): def is_dirty(self): """Return whether there are any pending changes.""" - return self._is_dirty if self._is_dirty in (False, True) else len(self._global_history) > 1 + return ( + self._is_dirty + if self._is_dirty in (False, True) + else len(self._global_history) > 1 + ) def undo(self): """Undo the last action.""" @@ -1159,11 +1247,14 @@ def save(self): spike_clusters = self.clustering.spike_clusters groups = { c: self.cluster_meta.get('group', c) or 'unsorted' - for c in self.clustering.cluster_ids} + for c in self.clustering.cluster_ids + } # List of tuples (field_name, dictionary). labels = [ - (field, self.get_labels(field)) for field in self.cluster_meta.fields - if field not in ('next_cluster')] + (field, self.get_labels(field)) + for field in self.cluster_meta.fields + if field not in ('next_cluster') + ] emit('save_clustering', self, spike_clusters, groups, *labels) # Cache the spikes_per_cluster array. self._save_spikes_per_cluster() diff --git a/phy/cluster/tests/conftest.py b/phy/cluster/tests/conftest.py index 26f2c8054..1add218bd 100644 --- a/phy/cluster/tests/conftest.py +++ b/phy/cluster/tests/conftest.py @@ -1,23 +1,22 @@ -# -*- coding: utf-8 -*- - """Test fixtures.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +from phylib.io.array import get_closest_clusters from pytest import fixture -from phylib.io.array import get_closest_clusters import phy.gui.qt # Reduce the debouncer delay for tests. phy.gui.qt.Debouncer.delay = 1 -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Fixtures -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + @fixture def cluster_ids(): @@ -41,4 +40,5 @@ def similarity(cluster_ids): def similarity(c): return get_closest_clusters(c, cluster_ids, sim) + return similarity diff --git a/phy/cluster/tests/test_clustering.py b/phy/cluster/tests/test_clustering.py index f920c8db8..201962bd1 100644 --- a/phy/cluster/tests/test_clustering.py +++ b/phy/cluster/tests/test_clustering.py @@ -1,27 +1,29 @@ -# -*- coding: utf-8 -*- - """Test clustering.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import numpy as np from numpy.testing import assert_array_equal as ae -from pytest import raises - +from phylib.io.array import ( + _spikes_in_clusters, +) from phylib.io.mock import artificial_spike_clusters -from phylib.io.array import (_spikes_in_clusters,) from phylib.utils import connect -from ..clustering import (_extend_spikes, - _concatenate_spike_clusters, - _extend_assignment, - Clustering) +from pytest import raises +from ..clustering import ( + Clustering, + _concatenate_spike_clusters, + _extend_assignment, + _extend_spikes, +) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test assignments -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_extend_spikes_simple(): spike_clusters = np.array([3, 5, 2, 9, 5, 5, 2]) @@ -62,10 +64,9 @@ def test_extend_spikes(): def test_concatenate_spike_clusters(): - spikes, clusters = _concatenate_spike_clusters(([1, 5, 4], - [10, 50, 40]), - ([2, 0, 3, 6], - [20, 0, 30, 60])) + spikes, clusters = _concatenate_spike_clusters( + ([1, 5, 4], [10, 50, 40]), ([2, 0, 3, 6], [20, 0, 30, 60]) + ) ae(spikes, np.arange(7)) ae(clusters, np.arange(0, 60 + 1, 10)) @@ -82,28 +83,31 @@ def test_extend_assignment(): # This should not depend on the index chosen. for to in (123, 0, 1, 2, 3): clusters_rel = [123] * len(spike_ids) - new_spike_ids, new_cluster_ids = _extend_assignment(spike_ids, - spike_clusters, - clusters_rel, - 10, - ) + new_spike_ids, new_cluster_ids = _extend_assignment( + spike_ids, + spike_clusters, + clusters_rel, + 10, + ) ae(new_spike_ids, [0, 2, 6]) ae(new_cluster_ids, [10, 10, 11]) # Second case: we assign the spikes to different clusters. clusters_rel = [0, 1] - new_spike_ids, new_cluster_ids = _extend_assignment(spike_ids, - spike_clusters, - clusters_rel, - 10, - ) + new_spike_ids, new_cluster_ids = _extend_assignment( + spike_ids, + spike_clusters, + clusters_rel, + 10, + ) ae(new_spike_ids, [0, 2, 6]) ae(new_cluster_ids, [10, 11, 12]) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test clustering -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_clustering_split(): spike_clusters = np.array([2, 5, 3, 2, 7, 5, 2]) @@ -115,22 +119,24 @@ def test_clustering_split(): assert clustering.n_spikes == n_spikes ae(clustering.spike_ids, np.arange(n_spikes)) - splits = [[0], - [1], - [2], - [0, 1], - [0, 2], - [1, 2], - [0, 1, 2], - [3], - [4], - [3, 4], - [6], - [6, 5], - [0, 6], - [0, 3, 6], - [0, 2, 6], - np.arange(7)] + splits = [ + [0], + [1], + [2], + [0, 1], + [0, 2], + [1, 2], + [0, 1, 2], + [3], + [4], + [3, 4], + [6], + [6, 5], + [0, 6], + [0, 3, 6], + [0, 2, 6], + np.arange(7), + ] # Test many splits. for to_split in splits: @@ -156,7 +162,7 @@ def test_clustering_descendants_merge(): up = clustering.merge([2, 3]) new = up.added[0] assert new == 8 - assert set(up.descendants) == set([(2, 8), (3, 8)]) + assert set(up.descendants) == {(2, 8), (3, 8)} with raises(ValueError): up = clustering.merge([2, 8]) @@ -164,7 +170,7 @@ def test_clustering_descendants_merge(): up = clustering.merge([5, 8]) new = up.added[0] assert new == 9 - assert set(up.descendants) == set([(5, 9), (8, 9)]) + assert set(up.descendants) == {(5, 9), (8, 9)} def test_clustering_descendants_split(): @@ -173,44 +179,44 @@ def test_clustering_descendants_split(): # Instantiate a Clustering instance. clustering = Clustering(spike_clusters) - with raises(Exception): + with raises(ValueError): clustering.split([-1]) - with raises(Exception): + with raises(ValueError): clustering.split([8]) # First split. up = clustering.split([0]) assert up.deleted == [2] assert up.added == [8, 9] - assert set(up.descendants) == set([(2, 8), (2, 9)]) + assert set(up.descendants) == {(2, 8), (2, 9)} ae(clustering.spike_clusters, [8, 5, 3, 9, 7, 5, 9]) # Undo. up = clustering.undo() assert up.deleted == [8, 9] assert up.added == [2] - assert set(up.descendants) == set([(8, 2), (9, 2)]) + assert set(up.descendants) == {(8, 2), (9, 2)} ae(clustering.spike_clusters, spike_clusters) # Redo. up = clustering.redo() assert up.deleted == [2] assert up.added == [8, 9] - assert set(up.descendants) == set([(2, 8), (2, 9)]) + assert set(up.descendants) == {(2, 8), (2, 9)} ae(clustering.spike_clusters, [8, 5, 3, 9, 7, 5, 9]) # Second split: just replace cluster 8 by 10 (1 spike in it). up = clustering.split([0]) assert up.deleted == [8] assert up.added == [10] - assert set(up.descendants) == set([(8, 10)]) + assert set(up.descendants) == {(8, 10)} ae(clustering.spike_clusters, [10, 5, 3, 9, 7, 5, 9]) # Undo again. up = clustering.undo() assert up.deleted == [10] assert up.added == [8] - assert set(up.descendants) == set([(10, 8)]) + assert set(up.descendants) == {(10, 8)} ae(clustering.spike_clusters, [8, 5, 3, 9, 7, 5, 9]) @@ -466,7 +472,7 @@ def test_clustering_long(): assert np.all(clustering.spike_clusters[:10] == new_cluster) # Merge. - my_spikes_0 = np.nonzero(np.in1d(clustering.spike_clusters, [2, 3]))[0] + my_spikes_0 = np.nonzero(np.isin(clustering.spike_clusters, [2, 3]))[0] info = clustering.merge([2, 3]) my_spikes = info.spike_ids ae(my_spikes, my_spikes_0) @@ -477,7 +483,7 @@ def test_clustering_long(): clustering.spike_clusters[:] = spike_clusters_base[:] clustering._new_cluster_id = 11 - my_spikes_0 = np.nonzero(np.in1d(clustering.spike_clusters, [4, 6]))[0] + my_spikes_0 = np.nonzero(np.isin(clustering.spike_clusters, [4, 6]))[0] info = clustering.merge([4, 6], 11) my_spikes = info.spike_ids ae(my_spikes, my_spikes_0) diff --git a/phy/cluster/tests/test_history.py b/phy/cluster/tests/test_history.py index af43c8780..6c6aa5acd 100644 --- a/phy/cluster/tests/test_history.py +++ b/phy/cluster/tests/test_history.py @@ -1,19 +1,17 @@ -# -*- coding: utf-8 -*- - """Tests of history structure.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import numpy as np -from .._history import History, GlobalHistory - +from .._history import GlobalHistory, History -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Tests -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_history(): history = History() @@ -76,19 +74,19 @@ def test_iter_history(): for i, item in enumerate(history): # Assert item if i > 0: - assert id(item) == id(locals()['item{0:d}'.format(i - 1)]) + assert id(item) == id(locals()[f'item{i - 1:d}']) for i, item in enumerate(history.iter(1, 2)): assert i == 0 # Assert item assert history.current_position == 3 - assert id(item) == id(locals()['item{0:d}'.format(i)]) + assert id(item) == id(locals()[f'item{i:d}']) for i, item in enumerate(history.iter(2, 3)): assert i == 0 # Assert item assert history.current_position == 3 - assert id(item) == id(locals()['item{0:d}'.format(i + 1)]) + assert id(item) == id(locals()[f'item{i + 1:d}']) def test_global_history(): @@ -141,4 +139,4 @@ def test_global_history(): assert gh.undo() == '' assert gh.redo() == 'h1 first' assert gh.redo() == 'h2 first' - assert gh.redo() == 'h1 second' + 'h2 second' + assert gh.redo() == 'h1 secondh2 second' diff --git a/phy/cluster/tests/test_supervisor.py b/phy/cluster/tests/test_supervisor.py index 1bcb98fc9..9cc986ec8 100644 --- a/phy/cluster/tests/test_supervisor.py +++ b/phy/cluster/tests/test_supervisor.py @@ -1,26 +1,30 @@ -# -*- coding: utf-8 -*- - """Test GUI component.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ -#from contextlib import contextmanager +# from contextlib import contextmanager -from pytest import fixture import numpy as np from numpy.testing import assert_array_equal as ae +from phylib.utils import Bunch, connect, emit +from pytest import fixture -from .. import supervisor as _supervisor -from ..supervisor import ( - Supervisor, TaskLogger, ClusterView, SimilarityView, ActionCreator) from phy.gui import GUI -from phy.gui.widgets import Barrier from phy.gui.qt import qInstallMessageHandler from phy.gui.tests.test_widgets import _assert, _wait_until_table_ready +from phy.gui.widgets import Barrier from phy.utils.context import Context -from phylib.utils import connect, Bunch, emit + +from .. import supervisor as _supervisor +from ..supervisor import ( + ActionCreator, + ClusterView, + SimilarityView, + Supervisor, + TaskLogger, +) def handler(msg_type, msg_log_context, msg_string): @@ -30,9 +34,10 @@ def handler(msg_type, msg_log_context, msg_string): qInstallMessageHandler(handler) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Fixtures -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + @fixture def gui(tempdir, qtbot): @@ -51,8 +56,7 @@ def gui(tempdir, qtbot): @fixture -def supervisor(qtbot, gui, cluster_ids, cluster_groups, cluster_labels, - similarity, tempdir): +def supervisor(qtbot, gui, cluster_ids, cluster_groups, cluster_labels, similarity, tempdir): spike_clusters = np.repeat(cluster_ids, 2) s = Supervisor( @@ -71,13 +75,14 @@ def supervisor(qtbot, gui, cluster_ids, cluster_groups, cluster_labels, return s -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test tasks -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + @fixture def tl(): - class MockClusterView(object): + class MockClusterView: _selected = [0] def select(self, cl, callback=None, **kwargs): @@ -93,7 +98,7 @@ def previous(self, callback=None): # pragma: no cover class MockSimilarityView(MockClusterView): pass - class MockSupervisor(object): + class MockSupervisor: def merge(self, cluster_ids, to, callback=None): callback(Bunch(deleted=cluster_ids, added=[to])) @@ -192,17 +197,22 @@ def test_task_move_all(tl): assert tl.last_state() == ([1], 2, [101], 102) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test cluster and similarity views -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + @fixture def data(): - _data = [{"id": i, - "n_spikes": 100 - 10 * i, - "group": {2: 'noise', 3: 'noise', 5: 'mua', 8: 'good'}.get(i, None), - "is_masked": i in (2, 3, 5), - } for i in range(10)] + _data = [ + { + 'id': i, + 'n_spikes': 100 - 10 * i, + 'group': {2: 'noise', 3: 'noise', 5: 'mua', 8: 'good'}.get(i), + 'is_masked': i in (2, 3, 5), + } + for i in range(10) + ] return _data @@ -232,7 +242,6 @@ def on_request_similar_clusters(sender, cluster_id): def test_cluster_view_extra_columns(qtbot, gui, data): - for cl in data: cl['my_metrics'] = cl['id'] * 1000 @@ -240,9 +249,10 @@ def test_cluster_view_extra_columns(qtbot, gui, data): _wait_until_table_ready(qtbot, cv) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test ActionCreator -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_action_creator_1(qtbot, gui): ac = ActionCreator() @@ -250,9 +260,10 @@ def test_action_creator_1(qtbot, gui): gui.show() -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test GUI component -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _select(supervisor, cluster_ids, similar=None): supervisor.task_logger.enqueue(supervisor.cluster_view, 'select', cluster_ids) @@ -295,12 +306,11 @@ def test_supervisor_busy(qtbot, supervisor): assert not supervisor._is_busy -def test_supervisor_cluster_metrics( - qtbot, gui, cluster_ids, cluster_groups, similarity, tempdir): +def test_supervisor_cluster_metrics(qtbot, gui, cluster_ids, cluster_groups, similarity, tempdir): spike_clusters = np.repeat(cluster_ids, 2) def my_metrics(cluster_id): - return cluster_id ** 2 + return cluster_id**2 cluster_metrics = {'my_metrics': my_metrics} @@ -344,7 +354,6 @@ def test_supervisor_select_order(qtbot, supervisor): def test_supervisor_edge_cases(supervisor): - # Empty selection at first. ae(supervisor.clustering.cluster_ids, [0, 1, 2, 10, 11, 20, 30]) @@ -386,7 +395,6 @@ def test_supervisor_save(qtbot, gui, supervisor): def test_supervisor_skip(qtbot, gui, supervisor): - # yield [0, 1, 2, 10, 11, 20, 30] # # i, g, N, i, g, N, N expected = [30, 20, 11, 2, 1] @@ -419,7 +427,6 @@ def test_supervisor_filter(qtbot, supervisor): def test_supervisor_merge_1(qtbot, supervisor): - _select(supervisor, [30], [20]) _assert_selected(supervisor, [30, 20]) @@ -477,7 +484,6 @@ def test_supervisor_merge_move(qtbot, supervisor): def test_supervisor_split_0(qtbot, supervisor): - _select(supervisor, [1, 2]) _assert_selected(supervisor, [1, 2]) @@ -496,7 +502,6 @@ def test_supervisor_split_0(qtbot, supervisor): def test_supervisor_split_1(supervisor): - supervisor.select_actions.select([1, 2]) supervisor.block() @@ -526,7 +531,6 @@ def test_supervisor_split_2(gui, similarity): def test_supervisor_state(tempdir, qtbot, gui, supervisor): - supervisor.select(1) cv = supervisor.cluster_view @@ -544,12 +548,11 @@ def test_supervisor_state(tempdir, qtbot, gui, supervisor): def test_supervisor_label(supervisor): - _select(supervisor, [20]) - supervisor.label("my_field", 3.14) + supervisor.label('my_field', 3.14) supervisor.block() - supervisor.label("my_field", 1.23, cluster_ids=30) + supervisor.label('my_field', 1.23, cluster_ids=30) supervisor.block() assert 'my_field' in supervisor.fields @@ -558,9 +561,8 @@ def test_supervisor_label(supervisor): def test_supervisor_label_cluster_1(supervisor): - _select(supervisor, [20, 30]) - supervisor.label("my_field", 3.14) + supervisor.label('my_field', 3.14) supervisor.block() # Same value for the old clusters. @@ -574,10 +576,9 @@ def test_supervisor_label_cluster_1(supervisor): def test_supervisor_label_cluster_2(supervisor): - _select(supervisor, [20]) - supervisor.label("my_field", 3.14) + supervisor.label('my_field', 3.14) supervisor.block() # One of the parents. @@ -592,10 +593,9 @@ def test_supervisor_label_cluster_2(supervisor): def test_supervisor_label_cluster_3(supervisor): - # Conflict: largest cluster wins. _select(supervisor, [20, 30]) - supervisor.label("my_field", 3.14) + supervisor.label('my_field', 3.14) supervisor.block() # Create merged cluster from 20 and 30. @@ -607,7 +607,7 @@ def test_supervisor_label_cluster_3(supervisor): assert supervisor.get_labels('my_field')[new] == 3.14 # Now, we label a smaller cluster. - supervisor.label("my_field", 2.718, cluster_ids=[10]) + supervisor.label('my_field', 2.718, cluster_ids=[10]) # We merge the large and small cluster together. up = supervisor.merge(up.added + [10]) @@ -618,7 +618,6 @@ def test_supervisor_label_cluster_3(supervisor): def test_supervisor_move_1(supervisor): - _select(supervisor, [20]) _assert_selected(supervisor, [20]) @@ -638,7 +637,6 @@ def test_supervisor_move_1(supervisor): def test_supervisor_move_2(supervisor): - _select(supervisor, [20], [10]) _assert_selected(supervisor, [20, 10]) @@ -656,7 +654,6 @@ def test_supervisor_move_2(supervisor): def test_supervisor_move_3(qtbot, supervisor): - supervisor.select_actions.next() supervisor.block() _assert_selected(supervisor, [30]) @@ -673,13 +670,12 @@ def test_supervisor_move_3(qtbot, supervisor): supervisor.block() _assert_selected(supervisor, [2]) - supervisor.cluster_meta.get('group', 30) == 'noise' - supervisor.cluster_meta.get('group', 20) == 'mua' - supervisor.cluster_meta.get('group', 11) == 'good' + assert supervisor.cluster_meta.get('group', 30) == 'noise' + assert supervisor.cluster_meta.get('group', 20) == 'mua' + assert supervisor.cluster_meta.get('group', 11) == 'good' def test_supervisor_move_4(supervisor): - _select(supervisor, [30], [20]) _assert_selected(supervisor, [30, 20]) @@ -695,9 +691,9 @@ def test_supervisor_move_4(supervisor): supervisor.block() _assert_selected(supervisor, [30, 1]) - supervisor.cluster_meta.get('group', 20) == 'noise' - supervisor.cluster_meta.get('group', 11) == 'mua' - supervisor.cluster_meta.get('group', 2) == 'good' + assert supervisor.cluster_meta.get('group', 20) == 'noise' + assert supervisor.cluster_meta.get('group', 11) == 'mua' + assert supervisor.cluster_meta.get('group', 2) == 'good' def test_supervisor_move_5(supervisor): @@ -720,18 +716,17 @@ def test_supervisor_move_5(supervisor): supervisor.block() _assert_selected(supervisor, []) - supervisor.cluster_meta.get('group', 30) == 'noise' - supervisor.cluster_meta.get('group', 20) == 'noise' + assert supervisor.cluster_meta.get('group', 30) == 'noise' + assert supervisor.cluster_meta.get('group', 20) == 'noise' - supervisor.cluster_meta.get('group', 11) == 'mua' - supervisor.cluster_meta.get('group', 10) == 'mua' + assert supervisor.cluster_meta.get('group', 11) == 'mua' + assert supervisor.cluster_meta.get('group', 10) == 'mua' - supervisor.cluster_meta.get('group', 2) == 'good' - supervisor.cluster_meta.get('group', 1) == 'good' + assert supervisor.cluster_meta.get('group', 2) == 'good' + assert supervisor.cluster_meta.get('group', 1) == 'good' def test_supervisor_reset(qtbot, supervisor): - supervisor.select_actions.select([10, 11]) supervisor.select_actions.reset_wizard() @@ -756,7 +751,6 @@ def test_supervisor_reset(qtbot, supervisor): def test_supervisor_nav(qtbot, supervisor): - supervisor.select_actions.reset_wizard() supervisor.block() _assert_selected(supervisor, [30]) diff --git a/phy/cluster/tests/test_utils.py b/phy/cluster/tests/test_utils.py index 720f90142..3cc4cdf83 100644 --- a/phy/cluster/tests/test_utils.py +++ b/phy/cluster/tests/test_utils.py @@ -1,31 +1,36 @@ -# -*- coding: utf-8 -*- - """Tests of manual clustering utility functions.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import logging from pytest import raises -from .._utils import (ClusterMeta, UpdateInfo, RotatingProperty, - _update_cluster_selection, create_cluster_meta) +from .._utils import ( + ClusterMeta, + RotatingProperty, + UpdateInfo, + _update_cluster_selection, + create_cluster_meta, +) logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Tests -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_create_cluster_meta(): - cluster_groups = {2: 3, - 3: 3, - 5: 1, - 7: 2, - } + cluster_groups = { + 2: 3, + 3: 3, + 5: 1, + 7: 2, + } meta = create_cluster_meta(cluster_groups) assert meta.group(2) == 3 assert meta.group(3) == 3 @@ -143,11 +148,12 @@ def test_metadata_history_complex(): def test_metadata_descendants(): """Test ClusterMeta history.""" - data = {0: {'group': 0}, - 1: {'group': 1}, - 2: {'group': 2}, - 3: {'group': 3}, - } + data = { + 0: {'group': 0}, + 1: {'group': 1}, + 2: {'group': 2}, + 3: {'group': 3}, + } meta = ClusterMeta() @@ -191,8 +197,7 @@ def test_update_info(): logger.debug(UpdateInfo(description='hello')) logger.debug(UpdateInfo(deleted=range(5), added=[5], description='merge')) logger.debug(UpdateInfo(deleted=range(5), added=[5], description='assign')) - logger.debug(UpdateInfo(deleted=range(5), added=[5], - description='assign', history='undo')) + logger.debug(UpdateInfo(deleted=range(5), added=[5], description='assign', history='undo')) logger.debug(UpdateInfo(metadata_changed=[2, 3], description='metadata')) diff --git a/phy/cluster/views/__init__.py b/phy/cluster/views/__init__.py index 5fdf76daa..36d01d50a 100644 --- a/phy/cluster/views/__init__.py +++ b/phy/cluster/views/__init__.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """Manual clustering views.""" diff --git a/phy/cluster/views/amplitude.py b/phy/cluster/views/amplitude.py index d4aea1d38..4bba0a19c 100644 --- a/phy/cluster/views/amplitude.py +++ b/phy/cluster/views/amplitude.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """Amplitude view.""" @@ -10,15 +8,15 @@ import logging import numpy as np - -from phy.utils.color import selected_cluster_color, add_alpha from phylib.utils._types import _as_array from phylib.utils.event import emit from phy.cluster._utils import RotatingProperty -from phy.plot.transform import Rotate, Scale, Translate, Range, NDC -from phy.plot.visuals import ScatterVisual, HistogramVisual, PatchVisual -from .base import ManualClusteringView, MarkerSizeMixin, LassoMixin +from phy.plot.transform import NDC, Range, Rotate, Scale, Translate +from phy.plot.visuals import HistogramVisual, PatchVisual, ScatterVisual +from phy.utils.color import add_alpha, selected_cluster_color + +from .base import LassoMixin, ManualClusteringView, MarkerSizeMixin from .histogram import _compute_histogram logger = logging.getLogger(__name__) @@ -28,6 +26,7 @@ # Amplitude view # ----------------------------------------------------------------------------- + class AmplitudeView(MarkerSizeMixin, LassoMixin, ManualClusteringView): """This view displays an amplitude plot for all selected clusters. @@ -49,20 +48,20 @@ class AmplitudeView(MarkerSizeMixin, LassoMixin, ManualClusteringView): _default_position = 'right' # Alpha channel of the markers in the scatter plot. - marker_alpha = 1. - time_range_color = (1., 1., 0., .25) + marker_alpha = 1.0 + time_range_color = (1.0, 1.0, 0.0, 0.25) # Number of bins in the histogram. n_bins = 100 # Alpha channel of the histogram in the background. - histogram_alpha = .5 + histogram_alpha = 0.5 # Quantile used for scaling of the amplitudes (less than 1 to avoid outliers). - quantile = .99 + quantile = 0.99 # Size of the histogram, between 0 and 1. - histogram_scale = .25 + histogram_scale = 0.25 default_shortcuts = { 'change_marker_size': 'alt+wheel', @@ -74,7 +73,7 @@ class AmplitudeView(MarkerSizeMixin, LassoMixin, ManualClusteringView): } def __init__(self, amplitudes=None, amplitudes_type=None, duration=None): - super(AmplitudeView, self).__init__() + super().__init__() self.state_attrs += ('amplitudes_type',) self.canvas.enable_axes() @@ -95,27 +94,33 @@ def __init__(self, amplitudes=None, amplitudes_type=None, duration=None): assert self.amplitudes_type in self.amplitudes self.cluster_ids = () - self.duration = duration or 1. + self.duration = duration or 1.0 # Histogram visual. self.hist_visual = HistogramVisual() - self.hist_visual.transforms.add([ - Range(NDC, (-1, -1, 1, -1 + 2 * self.histogram_scale)), - Rotate('cw'), - Scale((1, -1)), - Translate((2.05, 0)), - ]) + self.hist_visual.transforms.add( + [ + Range(NDC, (-1, -1, 1, -1 + 2 * self.histogram_scale)), + Rotate('cw'), + Scale((1, -1)), + Translate((2.05, 0)), + ] + ) self.canvas.add_visual(self.hist_visual) - self.canvas.panzoom.zoom = self.canvas.panzoom._default_zoom = (.75, 1) - self.canvas.panzoom.pan = self.canvas.panzoom._default_pan = (-.25, 0) + self.canvas.panzoom.zoom = self.canvas.panzoom._default_zoom = (0.75, 1) + self.canvas.panzoom.pan = self.canvas.panzoom._default_pan = (-0.25, 0) # Yellow vertical bar showing the selected time interval. self.patch_visual = PatchVisual(primitive_type='triangle_fan') - self.patch_visual.inserter.insert_vert(''' + self.patch_visual.inserter.insert_vert( + """ const float MIN_INTERVAL_SIZE = 0.01; uniform float u_interval_size; - ''', 'header') - self.patch_visual.inserter.insert_vert(''' + """, + 'header', + ) + self.patch_visual.inserter.insert_vert( + """ gl_Position.y = pos_orig.y; // The following is used to ensure that (1) the bar width increases with the zoom level @@ -126,7 +131,9 @@ def __init__(self, amplitudes=None, amplitudes_type=None, duration=None): // vertex is on the left or right edge of the bar. gl_Position.x += w * (-1 + 2 * int(a_position.z == 0)); - ''', 'after_transforms') + """, + 'after_transforms', + ) self.canvas.add_visual(self.patch_visual) # Scatter plot. @@ -140,11 +147,15 @@ def _get_data_bounds(self, bunchs): return (0, 0, self.duration, 1) m = min( np.quantile(bunch.amplitudes, 1 - self.quantile) - for bunch in bunchs if len(bunch.amplitudes)) + for bunch in bunchs + if len(bunch.amplitudes) + ) m = min(0, m) # ensure ymin <= 0 M = max( np.quantile(bunch.amplitudes, self.quantile) - for bunch in bunchs if len(bunch.amplitudes)) + for bunch in bunchs + if len(bunch.amplitudes) + ) return (0, m, self.duration, M) def _add_histograms(self, bunchs): @@ -164,14 +175,16 @@ def show_time_range(self, interval=(0, 0)): start, end = interval x0 = -1 + 2 * (start / self.duration) x1 = -1 + 2 * (end / self.duration) - xm = .5 * (x0 + x1) - pos = np.array([ - [xm, -1], - [xm, +1], - [xm, +1], - [xm, -1], - ]) - self.patch_visual.program['u_interval_size'] = .5 * (x1 - x0) + xm = 0.5 * (x0 + x1) + pos = np.array( + [ + [xm, -1], + [xm, +1], + [xm, +1], + [xm, -1], + ] + ) + self.patch_visual.program['u_interval_size'] = 0.5 * (x1 - x0) self.patch_visual.set_data(pos=pos, color=self.time_range_color, depth=[0, 0, 1, 1]) self.canvas.update() @@ -185,11 +198,13 @@ def _plot_cluster(self, bunch): self.hist_visual.add_batch_data( hist=bunch.histogram, ylim=self._ylim, - color=add_alpha(bunch.color, self.histogram_alpha)) + color=add_alpha(bunch.color, self.histogram_alpha), + ) # Scatter plot. self.visual.add_batch_data( - pos=bunch.pos, color=bunch.color, size=ms, data_bounds=self.data_bounds) + pos=bunch.pos, color=bunch.color, size=ms, data_bounds=self.data_bounds + ) def get_clusters_data(self, load_all=None): """Return a list of Bunch instances, with attributes pos and spike_ids.""" @@ -214,7 +229,9 @@ def get_clusters_data(self, load_all=None): bunch.color = ( selected_cluster_color(i - 1, self.marker_alpha) # Background amplitude color. - if cluster_id is not None else (.5, .5, .5, .5)) + if cluster_id is not None + else (0.5, 0.5, 0.5, 0.5) + ) return bunchs def plot(self, **kwargs): @@ -225,7 +242,7 @@ def plot(self, **kwargs): self.data_bounds = self._get_data_bounds(bunchs) bunchs = self._add_histograms(bunchs) # Use the same scale for all histograms. - self._ylim = max(bunch.histogram.max() for bunch in bunchs) if bunchs else 1. + self._ylim = max(bunch.histogram.max() for bunch in bunchs) if bunchs else 1.0 self.visual.reset_batch() self.hist_visual.reset_batch() @@ -240,20 +257,24 @@ def plot(self, **kwargs): def attach(self, gui): """Attach the view to the GUI.""" - super(AmplitudeView, self).attach(gui) + super().attach(gui) # Amplitude type actions. def _make_amplitude_action(a): def callback(): self.amplitudes_type = a self.plot() + return callback for a in self.amplitudes_types.keys(): - name = 'Change amplitudes type to %s' % a + name = f'Change amplitudes type to {a}' self.actions.add( - _make_amplitude_action(a), show_shortcut=False, - name=name, view_submenu='Change amplitudes type') + _make_amplitude_action(a), + show_shortcut=False, + name=name, + view_submenu='Change amplitudes type', + ) self.actions.add(self.next_amplitudes_type, set_busy=True) self.actions.add(self.previous_amplitudes_type, set_busy=True) @@ -273,13 +294,13 @@ def amplitudes_type(self, value): def next_amplitudes_type(self): """Switch to the next amplitudes type.""" self.amplitudes_types.next() - logger.debug("Switch to amplitudes type: %s.", self.amplitudes_types.current) + logger.debug('Switch to amplitudes type: %s.', self.amplitudes_types.current) self.plot() def previous_amplitudes_type(self): """Switch to the previous amplitudes type.""" self.amplitudes_types.previous() - logger.debug("Switch to amplitudes type: %s.", self.amplitudes_types.current) + logger.debug('Switch to amplitudes type: %s.', self.amplitudes_types.current) self.plot() def on_mouse_click(self, e): diff --git a/phy/cluster/views/base.py b/phy/cluster/views/base.py index 8dadc0214..ff3ea0510 100644 --- a/phy/cluster/views/base.py +++ b/phy/cluster/views/base.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """Manual clustering views.""" @@ -7,18 +5,18 @@ # Imports # ----------------------------------------------------------------------------- -from functools import partial import gc import logging +from functools import partial import numpy as np - -from phylib.utils import Bunch, connect, unconnect, emit +from phylib.utils import Bunch, connect, emit, unconnect from phylib.utils.geometry import range_transform + from phy.cluster._utils import RotatingProperty from phy.gui import Actions -from phy.gui.qt import AsyncCaller, screenshot, screenshot_default_path, thread_pool, Worker -from phy.plot import PlotCanvas, NDC, extend_bounds +from phy.gui.qt import AsyncCaller, Worker, screenshot, screenshot_default_path, thread_pool +from phy.plot import NDC, PlotCanvas, extend_bounds from phy.utils.color import ClusterColorSelector logger = logging.getLogger(__name__) @@ -28,6 +26,7 @@ # Manual clustering view # ----------------------------------------------------------------------------- + def _get_bunch_bounds(bunch): """Return the data bounds of a bunch.""" if 'data_bounds' in bunch and bunch.data_bounds is not None: @@ -37,7 +36,7 @@ def _get_bunch_bounds(bunch): return (xmin, ymin, xmax, ymax) -class ManualClusteringView(object): +class ManualClusteringView: """Base class for clustering views. Typical property objects: @@ -58,6 +57,7 @@ class ManualClusteringView(object): - `toggle_auto_update(view)` """ + default_shortcuts = {} default_snippets = {} auto_update = True # automatically update the view when the cluster selection changes @@ -108,7 +108,6 @@ def _plot_cluster(self, bunch): To override. """ - pass def _update_axes(self): """Update the axes.""" @@ -213,7 +212,7 @@ def finished(): # HACK: disable threading mechanism for now # if getattr(gui, '_enable_threading', True): - if 0: # pragma: no cover + if 0: # pragma: no cover # This is only for OpenGL views. self.canvas.set_lazy(True) thread_pool().start(worker) @@ -255,12 +254,17 @@ def attach(self, gui): self.set_state(gui.state.get_view_state(self)) self.actions = Actions( - gui, name=self.name, view=self, - default_shortcuts=shortcuts, default_snippets=self.default_snippets) + gui, + name=self.name, + view=self, + default_shortcuts=shortcuts, + default_snippets=self.default_snippets, + ) # Freeze and unfreeze the view when selecting clusters. self.actions.add( - self.toggle_auto_update, checkable=True, checked=self.auto_update, show_shortcut=False) + self.toggle_auto_update, checkable=True, checked=self.auto_update, show_shortcut=False + ) self.actions.add(self.screenshot, show_shortcut=False) self.actions.add(self.close, show_shortcut=False) self.actions.separator() @@ -273,7 +277,7 @@ def attach(self, gui): def on_close_view(view_, gui): if view_ != self: return - logger.debug("Close view %s.", self.name) + logger.debug('Close view %s.', self.name) self._closed = True gui.remove_menu(self.name) unconnect(on_select) @@ -301,7 +305,7 @@ def status(self): def update_status(self): if hasattr(self, 'dock'): - self.dock.set_status('%s %s' % (self.status, self.ex_status)) + self.dock.set_status(f'{self.status} {self.ex_status}') # ------------------------------------------------------------------------- # Misc public methods @@ -309,7 +313,7 @@ def update_status(self): def toggle_auto_update(self, checked): """When on, the view is automatically updated when the cluster selection changes.""" - logger.debug("%s auto update for %s.", 'Enable' if checked else 'Disable', self.name) + logger.debug('%s auto update for %s.', 'Enable' if checked else 'Disable', self.name) self.auto_update = checked emit('toggle_auto_update', self, checked) @@ -334,7 +338,7 @@ def set_state(self, state): May be overriden. """ - logger.debug("Set state for %s.", getattr(self, 'name', self.__class__.__name__)) + logger.debug('Set state for %s.', getattr(self, 'name', self.__class__.__name__)) for k, v in state.items(): setattr(self, k, v) @@ -356,12 +360,13 @@ def close(self): # Mixins for manual clustering views # ----------------------------------------------------------------------------- -class BaseWheelMixin(object): + +class BaseWheelMixin: def on_mouse_wheel(self, e): pass -class BaseGlobalView(object): +class BaseGlobalView: """A view that shows all clusters instead of the selected clusters. This view shows the clusters in the same order as in the cluster view. It reacts to sorting @@ -420,11 +425,10 @@ def on_select(self, sender=None, cluster_ids=(), **kwargs): class BaseColorView(BaseWheelMixin): - """Provide facilities to add and select color schemes in the view. - """ + """Provide facilities to add and select color schemes in the view.""" def __init__(self, *args, **kwargs): - super(BaseColorView, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.state_attrs += ('color_scheme',) # Color schemes. @@ -432,8 +436,14 @@ def __init__(self, *args, **kwargs): self.add_color_scheme(fun=0, name='blank', colormap='blank', categorical=True) def add_color_scheme( - self, fun=None, name=None, cluster_ids=None, - colormap=None, categorical=None, logarithmic=None): + self, + fun=None, + name=None, + cluster_ids=None, + colormap=None, + categorical=None, + logarithmic=None, + ): """Add a color scheme to the view. Can be used as follows: ```python @@ -445,24 +455,33 @@ def on_view_attached(gui, view): """ if fun is None: return partial( - self.add_color_scheme, name=name, cluster_ids=cluster_ids, - colormap=colormap, categorical=categorical, logarithmic=logarithmic) + self.add_color_scheme, + name=name, + cluster_ids=cluster_ids, + colormap=colormap, + categorical=categorical, + logarithmic=logarithmic, + ) field = name or fun.__name__ cs = ClusterColorSelector( - fun, cluster_ids=cluster_ids, - colormap=colormap, categorical=categorical, logarithmic=logarithmic) + fun, + cluster_ids=cluster_ids, + colormap=colormap, + categorical=categorical, + logarithmic=logarithmic, + ) self.color_schemes.add(field, cs) def get_cluster_colors(self, cluster_ids, alpha=1.0): """Return the cluster colors depending on the currently-selected color scheme.""" cs = self.color_schemes.get() if cs is None: # pragma: no cover - raise RuntimeError("Make sure that at least a color scheme is added.") + raise RuntimeError('Make sure that at least a color scheme is added.') return cs.get_colors(cluster_ids, alpha=alpha) def _neighbor_color_scheme(self, dir=+1): name = self.color_schemes._neighbor(dir=dir) - logger.debug("Switch to `%s` color scheme in %s.", name, self.__class__.__name__) + logger.debug('Switch to `%s` color scheme in %s.', name, self.__class__.__name__) self.update_color() self.update_select_color() self.update_status() @@ -477,11 +496,9 @@ def previous_color_scheme(self): def update_color(self): """Update the cluster colors depending on the current color scheme. To be overriden.""" - pass def update_select_color(self): """Update the cluster colors after the cluster selection changes.""" - pass @property def color_scheme(self): @@ -491,13 +508,13 @@ def color_scheme(self): @color_scheme.setter def color_scheme(self, color_scheme): """Change the current color scheme.""" - logger.debug("Set color scheme to %s.", color_scheme) + logger.debug('Set color scheme to %s.', color_scheme) self.color_schemes.set(color_scheme) self.update_color() self.update_status() def attach(self, gui): - super(BaseColorView, self).attach(gui) + super().attach(gui) # Set the current color scheme to the GUI state color scheme (automatically set # in self.color_scheme). self.color_schemes.set(self.color_scheme) @@ -506,13 +523,17 @@ def attach(self, gui): def _make_color_scheme_action(cs): def callback(): self.color_scheme = cs + return callback for cs in self.color_schemes.keys(): - name = 'Change color scheme to %s' % cs + name = f'Change color scheme to {cs}' self.actions.add( - _make_color_scheme_action(cs), show_shortcut=False, - name=name, view_submenu='Change color scheme') + _make_color_scheme_action(cs), + show_shortcut=False, + name=name, + view_submenu='Change color scheme', + ) self.actions.add(self.next_color_scheme) self.actions.add(self.previous_color_scheme) @@ -520,7 +541,7 @@ def callback(): def on_mouse_wheel(self, e): # pragma: no cover """Change the scaling with the wheel.""" - super(BaseColorView, self).on_mouse_wheel(e) + super().on_mouse_wheel(e) if e.modifiers == ('Shift',): if e.delta > 0: self.next_color_scheme() @@ -532,6 +553,7 @@ class ScalingMixin(BaseWheelMixin): """Provide features to change the scaling. Implement increase, decrease, reset actions, as well as control+wheel shortcut.""" + _scaling_param_increment = 1.1 _scaling_param_min = 1e-3 _scaling_param_max = 1e3 @@ -539,7 +561,7 @@ class ScalingMixin(BaseWheelMixin): _scaling_modifiers = ('Control',) def attach(self, gui): - super(ScalingMixin, self).attach(gui) + super().attach(gui) self.actions.add(self.increase) self.actions.add(self.decrease) self.actions.add(self.reset_scaling) @@ -547,7 +569,7 @@ def attach(self, gui): def on_mouse_wheel(self, e): # pragma: no cover """Change the scaling with the wheel.""" - super(ScalingMixin, self).on_mouse_wheel(e) + super().on_mouse_wheel(e) if e.modifiers == self._scaling_modifiers: if e.delta > 0: self.increase() @@ -565,14 +587,16 @@ def _set_scaling_value(self, value): # pragma: no cover def increase(self): """Increase the scaling parameter.""" value = self._get_scaling_value() - self._set_scaling_value(min( - self._scaling_param_max, value * self._scaling_param_increment)) + self._set_scaling_value( + min(self._scaling_param_max, value * self._scaling_param_increment) + ) def decrease(self): """Decrease the scaling parameter.""" value = self._get_scaling_value() - self._set_scaling_value(max( - self._scaling_param_min, value / self._scaling_param_increment)) + self._set_scaling_value( + max(self._scaling_param_min, value / self._scaling_param_increment) + ) def reset_scaling(self): """Reset the scaling to the default value.""" @@ -580,14 +604,14 @@ def reset_scaling(self): class MarkerSizeMixin(BaseWheelMixin): - _marker_size = 5. - _default_marker_size = 5. + _marker_size = 5.0 + _default_marker_size = 5.0 _marker_size_min = 1e-2 _marker_size_max = 1e2 _marker_size_increment = 1.1 def __init__(self, *args, **kwargs): - super(MarkerSizeMixin, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.state_attrs += ('marker_size',) self.local_state_attrs += () @@ -607,7 +631,7 @@ def marker_size(self, val): self.canvas.update() def attach(self, gui): - super(MarkerSizeMixin, self).attach(gui) + super().attach(gui) self.actions.add(self.increase_marker_size) self.actions.add(self.decrease_marker_size) self.actions.add(self.reset_marker_size) @@ -616,12 +640,14 @@ def attach(self, gui): def increase_marker_size(self): """Increase the scaling parameter.""" self.marker_size = min( - self._marker_size_max, self.marker_size * self._marker_size_increment) + self._marker_size_max, self.marker_size * self._marker_size_increment + ) def decrease_marker_size(self): """Decrease the scaling parameter.""" self.marker_size = max( - self._marker_size_min, self.marker_size / self._marker_size_increment) + self._marker_size_min, self.marker_size / self._marker_size_increment + ) def reset_marker_size(self): """Reset the scaling to the default value.""" @@ -629,7 +655,7 @@ def reset_marker_size(self): def on_mouse_wheel(self, e): # pragma: no cover """Change the scaling with the wheel.""" - super(MarkerSizeMixin, self).on_mouse_wheel(e) + super().on_mouse_wheel(e) if e.modifiers == ('Alt',): if e.delta > 0: self.increase_marker_size() @@ -637,10 +663,10 @@ def on_mouse_wheel(self, e): # pragma: no cover self.decrease_marker_size() -class LassoMixin(object): +class LassoMixin: def on_request_split(self, sender=None): """Return the spikes enclosed by the lasso.""" - if (self.canvas.lasso.count < 3 or not len(self.cluster_ids)): # pragma: no cover + if self.canvas.lasso.count < 3 or not len(self.cluster_ids): # pragma: no cover return np.array([], dtype=np.int64) # Get all points from all clusters. @@ -661,7 +687,7 @@ def on_request_split(self, sender=None): pos.append(points) spike_ids.append(bunch.spike_ids) if not pos: # pragma: no cover - logger.warning("Empty lasso.") + logger.warning('Empty lasso.') return np.array([]) pos = np.vstack(pos) pos = range_transform([self.data_bounds], [NDC], pos) @@ -673,5 +699,5 @@ def on_request_split(self, sender=None): return np.unique(spike_ids[ind]) def attach(self, gui): - super(LassoMixin, self).attach(gui) + super().attach(gui) connect(self.on_request_split) diff --git a/phy/cluster/views/cluscatter.py b/phy/cluster/views/cluscatter.py index 9fa9dd09c..1220b48c6 100644 --- a/phy/cluster/views/cluscatter.py +++ b/phy/cluster/views/cluscatter.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """Cluster scatter view.""" @@ -10,13 +8,13 @@ import logging import numpy as np +from phylib.utils import connect, emit, unconnect +from phy.plot.transform import NDC, range_transform +from phy.plot.visuals import ScatterVisual, TextVisual from phy.utils.color import _add_selected_clusters_colors -from phylib.utils import emit, connect, unconnect -from phy.plot.transform import range_transform, NDC -from phy.plot.visuals import ScatterVisual, TextVisual -from .base import ManualClusteringView, BaseGlobalView, MarkerSizeMixin, BaseColorView +from .base import BaseColorView, BaseGlobalView, ManualClusteringView, MarkerSizeMixin logger = logging.getLogger(__name__) @@ -25,6 +23,7 @@ # Template view # ----------------------------------------------------------------------------- + class ClusterScatterView(MarkerSizeMixin, BaseColorView, BaseGlobalView, ManualClusteringView): """This view shows all clusters in a customizable scatter plot. @@ -38,16 +37,17 @@ class ClusterScatterView(MarkerSizeMixin, BaseColorView, BaseGlobalView, ManualC Maps plot dimension to cluster attributes. """ + _default_position = 'right' - _scaling = 1. - _default_alpha = .75 + _scaling = 1.0 + _default_alpha = 0.75 _min_marker_size = 5.0 _max_marker_size = 30.0 _dims = ('x_axis', 'y_axis', 'size') # NOTE: this is not the actual marker size, but a scaling factor for the normal marker size. - _marker_size = 1. - _default_marker_size = 1. + _marker_size = 1.0 + _default_marker_size = 1.0 x_axis = '' y_axis = '' @@ -71,13 +71,16 @@ class ClusterScatterView(MarkerSizeMixin, BaseColorView, BaseGlobalView, ManualC 'set_size': 'css', } - def __init__( - self, cluster_ids=None, cluster_info=None, bindings=None, **kwargs): - super(ClusterScatterView, self).__init__(**kwargs) + def __init__(self, cluster_ids=None, cluster_info=None, bindings=None, **kwargs): + super().__init__(**kwargs) self.state_attrs += ( 'scaling', - 'x_axis', 'y_axis', 'size', - 'x_axis_log_scale', 'y_axis_log_scale', 'size_log_scale', + 'x_axis', + 'y_axis', + 'size', + 'x_axis_log_scale', + 'y_axis_log_scale', + 'size_log_scale', ) self.local_state_attrs += () @@ -105,8 +108,10 @@ def __init__( def _update_labels(self): self.label_visual.set_data( - pos=[[-1, -1], [1, 1]], text=[self.x_axis, self.y_axis], - anchor=[[1.25, 3], [-3, -1.25]]) + pos=[[-1, -1], [1, 1]], + text=[self.x_axis, self.y_axis], + anchor=[[1.25, 3], [-3, -1.25]], + ) # Data access # ------------------------------------------------------------------------- @@ -118,7 +123,7 @@ def bindings(self): def get_cluster_data(self, cluster_id): """Return the data of one cluster.""" data = self.cluster_info(cluster_id) - return {k: data.get(v, 0.) for k, v in self.bindings.items()} + return {k: data.get(v, 0.0) for k, v in self.bindings.items()} def get_clusters_data(self, cluster_ids): """Return the data of a set of clusters, as a dictionary {cluster_id: Bunch}.""" @@ -156,13 +161,15 @@ def prepare_position(self): # Create the x array. x = np.array( - [self.cluster_data[cluster_id]['x_axis'] or 0. for cluster_id in self.all_cluster_ids]) + [self.cluster_data[cluster_id]['x_axis'] or 0.0 for cluster_id in self.all_cluster_ids] + ) if self.x_axis_log_scale: x = np.log(1.0 + x - x.min()) # Create the y array. y = np.array( - [self.cluster_data[cluster_id]['y_axis'] or 0. for cluster_id in self.all_cluster_ids]) + [self.cluster_data[cluster_id]['y_axis'] or 0.0 for cluster_id in self.all_cluster_ids] + ) if self.y_axis_log_scale: y = np.log(1.0 + y - y.min()) @@ -174,7 +181,8 @@ def prepare_position(self): def prepare_size(self): """Compute the marker sizes.""" size = np.array( - [self.cluster_data[cluster_id]['size'] or 1. for cluster_id in self.all_cluster_ids]) + [self.cluster_data[cluster_id]['size'] or 1.0 for cluster_id in self.all_cluster_ids] + ) # Log scale for the size. if self.size_log_scale: size = np.log(1.0 + size - size.min()) @@ -229,7 +237,8 @@ def update_select_color(self): selected_clusters = self.cluster_ids if selected_clusters is not None and len(selected_clusters) > 0: colors = _add_selected_clusters_colors( - selected_clusters, self.all_cluster_ids, self.marker_colors.copy()) + selected_clusters, self.all_cluster_ids, self.marker_colors.copy() + ) self.visual.set_color(colors) self.canvas.update() @@ -238,9 +247,11 @@ def plot(self, **kwargs): if self.marker_positions is None: self.prepare_data() self.visual.set_data( - pos=self.marker_positions, color=self.marker_colors, + pos=self.marker_positions, + color=self.marker_colors, size=self.marker_sizes * self._marker_size, # marker size scaling factor - data_bounds=self.data_bounds) + data_bounds=self.data_bounds, + ) self.canvas.axes.reset_data_bounds(self.data_bounds) self.canvas.update() @@ -261,7 +272,7 @@ def change_bindings(self, **kwargs): def toggle_log_scale(self, dim, checked): """Toggle logarithmic scaling for one of the dimensions.""" self._size_min = None - setattr(self, '%s_log_scale' % dim, checked) + setattr(self, f'{dim}_log_scale', checked) self.prepare_data() self.plot() self.canvas.update() @@ -283,34 +294,43 @@ def set_size(self, field): def attach(self, gui): """Attach the GUI.""" - super(ClusterScatterView, self).attach(gui) + super().attach(gui) def _make_action(dim, name): def callback(): self.change_bindings(**{dim: name}) + return callback def _make_log_toggle(dim): def callback(checked): self.toggle_log_scale(dim, checked) + return callback # Change the bindings. for dim in self._dims: - view_submenu = 'Change %s' % dim + view_submenu = f'Change {dim}' # Change to every cluster info. for name in self.fields: self.actions.add( - _make_action(dim, name), show_shortcut=False, - name='Change %s to %s' % (dim, name), view_submenu=view_submenu) + _make_action(dim, name), + show_shortcut=False, + name=f'Change {dim} to {name}', + view_submenu=view_submenu, + ) # Toggle logarithmic scale. self.actions.separator(view_submenu=view_submenu) self.actions.add( - _make_log_toggle(dim), checkable=True, view_submenu=view_submenu, - name='Toggle log scale for %s' % dim, show_shortcut=False, - checked=getattr(self, '%s_log_scale' % dim)) + _make_log_toggle(dim), + checkable=True, + view_submenu=view_submenu, + name=f'Toggle log scale for {dim}', + show_shortcut=False, + checked=getattr(self, f'{dim}_log_scale'), + ) self.actions.separator() self.actions.add(self.set_x_axis, prompt=True, prompt_default=lambda: self.x_axis) @@ -327,7 +347,7 @@ def on_lasso_updated(sender, polygon): pos = range_transform([self.data_bounds], [NDC], self.marker_positions) ind = self.canvas.lasso.in_polygon(pos) cluster_ids = self.all_cluster_ids[ind] - emit("request_select", self, list(cluster_ids)) + emit('request_select', self, list(cluster_ids)) @connect(sender=self) def on_close_view(view_, gui): @@ -341,7 +361,7 @@ def on_close_view(view_, gui): self._update_labels() def on_select(self, *args, **kwargs): - super(ClusterScatterView, self).on_select(*args, **kwargs) + super().on_select(*args, **kwargs) self.update_select_color() def on_cluster(self, sender, up): @@ -351,7 +371,7 @@ def on_cluster(self, sender, up): @property def status(self): - return 'Size: %s. Color scheme: %s.' % (self.size, self.color_scheme) + return f'Size: {self.size}. Color scheme: {self.color_scheme}.' # Interactivity # ------------------------------------------------------------------------- @@ -365,7 +385,7 @@ def on_mouse_click(self, e): marker_pos = range_transform([self.data_bounds], [NDC], self.marker_positions) cluster_rel = np.argmin(((marker_pos - pos) ** 2).sum(axis=1)) cluster_id = self.all_cluster_ids[cluster_rel] - logger.debug("Click on cluster %d with button %s.", cluster_id, b) + logger.debug('Click on cluster %d with button %s.', cluster_id, b) if 'Shift' in e.modifiers: emit('select_more', self, [cluster_id]) else: diff --git a/phy/cluster/views/correlogram.py b/phy/cluster/views/correlogram.py index d6a3f321a..2a1d9d612 100644 --- a/phy/cluster/views/correlogram.py +++ b/phy/cluster/views/correlogram.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """Correlogram view.""" @@ -10,12 +8,13 @@ import logging import numpy as np +from phylib.io.array import _clip +from phylib.utils import Bunch from phy.plot.transform import Scale from phy.plot.visuals import HistogramVisual, LineVisual, TextVisual -from phylib.io.array import _clip -from phylib.utils import Bunch -from phy.utils.color import selected_cluster_color, _override_hsv, add_alpha +from phy.utils.color import _override_hsv, add_alpha, selected_cluster_color + from .base import ManualClusteringView, ScalingMixin logger = logging.getLogger(__name__) @@ -25,6 +24,7 @@ # Correlogram view # ----------------------------------------------------------------------------- + class CorrelogramView(ScalingMixin, ManualClusteringView): """A view showing the autocorrelogram of the selected clusters, and all cross-correlograms of cluster pairs. @@ -70,14 +70,18 @@ class CorrelogramView(ScalingMixin, ManualClusteringView): } def __init__(self, correlograms=None, firing_rate=None, sample_rate=None, **kwargs): - super(CorrelogramView, self).__init__(**kwargs) + super().__init__(**kwargs) self.state_attrs += ( - 'bin_size', 'window_size', 'refractory_period', 'uniform_normalization') + 'bin_size', + 'window_size', + 'refractory_period', + 'uniform_normalization', + ) self.local_state_attrs += () self.canvas.set_layout(layout='grid') # Outside margin to show labels. - self.canvas.gpu_transforms.add(Scale(.9)) + self.canvas.gpu_transforms.add(Scale(0.9)) assert sample_rate > 0 self.sample_rate = float(sample_rate) @@ -97,7 +101,7 @@ def __init__(self, correlograms=None, firing_rate=None, sample_rate=None, **kwar self.line_visual = LineVisual() self.canvas.add_visual(self.line_visual) - self.text_visual = TextVisual(color=(1., 1., 1., 1.)) + self.text_visual = TextVisual(color=(1.0, 1.0, 1.0, 1.0)) self.canvas.add_visual(self.text_visual) # ------------------------------------------------------------------------- @@ -127,23 +131,27 @@ def get_clusters_data(self, load_all=None): b.pair_index = i, j b.color = selected_cluster_color(i, 1) if i != j: - b.color = add_alpha(_override_hsv(b.color[:3], s=.1, v=1)) + b.color = add_alpha(_override_hsv(b.color[:3], s=0.1, v=1)) bunchs.append(b) return bunchs def _plot_pair(self, bunch): # Plot the histogram. self.correlogram_visual.add_batch_data( - hist=bunch.correlogram, color=bunch.color, - ylim=bunch.data_bounds[3], box_index=bunch.pair_index) + hist=bunch.correlogram, + color=bunch.color, + ylim=bunch.data_bounds[3], + box_index=bunch.pair_index, + ) # Plot the firing rate. - gray = (.25, .25, .25, 1.) + gray = (0.25, 0.25, 0.25, 1.0) if bunch.firing_rate is not None: # Line. pos = np.array([[0, bunch.firing_rate, bunch.data_bounds[2], bunch.firing_rate]]) self.line_visual.add_batch_data( - pos=pos, color=gray, data_bounds=bunch.data_bounds, box_index=bunch.pair_index) + pos=pos, color=gray, data_bounds=bunch.data_bounds, box_index=bunch.pair_index + ) # # Text. # self.text_visual.add_batch_data( # pos=[bunch.data_bounds[2], bunch.firing_rate], @@ -154,12 +162,13 @@ def _plot_pair(self, bunch): # ) # Refractory period. - xrp0 = round((self.window_size * .5 - self.refractory_period) / self.bin_size) - xrp1 = round((self.window_size * .5 + self.refractory_period) / self.bin_size) + 1 + xrp0 = round((self.window_size * 0.5 - self.refractory_period) / self.bin_size) + xrp1 = round((self.window_size * 0.5 + self.refractory_period) / self.bin_size) + 1 ylim = bunch.data_bounds[3] pos = np.array([[xrp0, 0, xrp0, ylim], [xrp1, 0, xrp1, ylim]]) self.line_visual.add_batch_data( - pos=pos, color=gray, data_bounds=bunch.data_bounds, box_index=bunch.pair_index) + pos=pos, color=gray, data_bounds=bunch.data_bounds, box_index=bunch.pair_index + ) def _plot_labels(self): n = len(self.cluster_ids) @@ -228,19 +237,21 @@ def toggle_labels(self, checked): def attach(self, gui): """Attach the view to the GUI.""" - super(CorrelogramView, self).attach(gui) + super().attach(gui) self.actions.add(self.toggle_normalization, shortcut='n', checkable=True) self.actions.add(self.toggle_labels, checkable=True, checked=True) self.actions.separator() + self.actions.add(self.set_bin, prompt=True, prompt_default=lambda: self.bin_size * 1000) self.actions.add( - self.set_bin, prompt=True, prompt_default=lambda: self.bin_size * 1000) - self.actions.add( - self.set_window, prompt=True, prompt_default=lambda: self.window_size * 1000) + self.set_window, prompt=True, prompt_default=lambda: self.window_size * 1000 + ) self.actions.add( - self.set_refractory_period, prompt=True, - prompt_default=lambda: self.refractory_period * 1000) + self.set_refractory_period, + prompt=True, + prompt_default=lambda: self.refractory_period * 1000, + ) self.actions.separator() # ------------------------------------------------------------------------- @@ -263,11 +274,11 @@ def _set_bin_window(self, bin_size=None, window_size=None): @property def status(self): b, w = self.bin_size * 1000, self.window_size * 1000 - return '{:.1f} ms ({:.1f} ms)'.format(w, b) + return f'{w:.1f} ms ({b:.1f} ms)' def set_refractory_period(self, value): """Set the refractory period (in milliseconds).""" - self.refractory_period = _clip(value, .1, 100) * 1e-3 + self.refractory_period = _clip(value, 0.1, 100) * 1e-3 self.plot() def set_bin(self, bin_size): @@ -298,7 +309,7 @@ def decrease(self): def on_mouse_wheel(self, e): # pragma: no cover """Change the scaling with the wheel.""" - super(CorrelogramView, self).on_mouse_wheel(e) + super().on_mouse_wheel(e) if e.modifiers == ('Alt',): - self._set_bin_window(bin_size=self.bin_size * 1.1 ** e.delta) + self._set_bin_window(bin_size=self.bin_size * 1.1**e.delta) self.plot() diff --git a/phy/cluster/views/feature.py b/phy/cluster/views/feature.py index dd8bd39c6..772d92749 100644 --- a/phy/cluster/views/feature.py +++ b/phy/cluster/views/feature.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """Feature view.""" @@ -11,11 +9,12 @@ import re import numpy as np - from phylib.utils import Bunch, emit -from phy.utils.color import selected_cluster_color + from phy.plot.transform import Range -from phy.plot.visuals import ScatterVisual, TextVisual, LineVisual +from phy.plot.visuals import LineVisual, ScatterVisual, TextVisual +from phy.utils.color import selected_cluster_color + from .base import ManualClusteringView, MarkerSizeMixin, ScalingMixin logger = logging.getLogger(__name__) @@ -25,6 +24,7 @@ # Feature view # ----------------------------------------------------------------------------- + def _get_default_grid(): """In the grid specification, 0 corresponds to the best channel, 1 to the second best, and so on. A, B, C refer to the PC components.""" @@ -38,18 +38,15 @@ def _get_default_grid(): def _get_point_color(clu_idx=None): - if clu_idx is not None: - color = selected_cluster_color(clu_idx, .5) - else: - color = (.5,) * 4 + color = selected_cluster_color(clu_idx, 0.5) if clu_idx is not None else (0.5,) * 4 assert len(color) == 4 return color def _get_point_masks(masks=None, clu_idx=None): - masks = masks if masks is not None else 1. + masks = masks if masks is not None else 1.0 # NOTE: we add the cluster relative index for the computation of the depth on the GPU. - return masks * .99999 + (clu_idx or 0) + return masks * 0.99999 + (clu_idx or 0) def _get_masks_max(px, py): @@ -97,7 +94,7 @@ class FeatureView(MarkerSizeMixin, ScalingMixin, ManualClusteringView): # Whether to disable automatic selection of channels. fixed_channels = False - feature_scaling = 1. + feature_scaling = 1.0 default_shortcuts = { 'change_marker_size': 'alt+wheel', @@ -109,14 +106,16 @@ class FeatureView(MarkerSizeMixin, ScalingMixin, ManualClusteringView): } def __init__(self, features=None, attributes=None, **kwargs): - super(FeatureView, self).__init__(**kwargs) + super().__init__(**kwargs) self.state_attrs += ('fixed_channels', 'feature_scaling') assert features self.features = features self._lim = 1 - self.grid_dim = _get_default_grid() # 2D array where every item a string like `0A,1B` + self.grid_dim = ( + _get_default_grid() + ) # 2D array where every item a string like `0A,1B` self.n_rows, self.n_cols = np.array(self.grid_dim).shape self.canvas.set_layout('grid', shape=(self.n_rows, self.n_cols)) self.canvas.enable_lasso() @@ -226,7 +225,7 @@ def _plot_points(self, bunch, clu_idx=None): py = self._get_axis_data(bunch, dim_y, cluster_id=cluster_id) # Skip empty data. if px is None or py is None: # pragma: no cover - logger.warning("Skipping empty data for cluster %d.", cluster_id) + logger.warning('Skipping empty data for cluster %d.', cluster_id) return assert px.data.shape == py.data.shape xmin, xmax = self._get_axis_bounds(dim_x, px) @@ -238,7 +237,8 @@ def _plot_points(self, bunch, clu_idx=None): # Prepare the batch visual with all subplots # for the selected cluster. self.visual.add_batch_data( - x=px.data, y=py.data, + x=px.data, + y=py.data, color=_get_point_color(clu_idx), # Reduced marker size for background features size=self._marker_size, @@ -260,7 +260,7 @@ def _plot_points(self, bunch, clu_idx=None): box_index=(i, j), ) self.text_visual.add_batch_data( - pos=[0, -1.], + pos=[0, -1.0], anchor=[0, 1], text=label_x, data_bounds=None, @@ -271,9 +271,8 @@ def _plot_axes(self): self.line_visual.reset_batch() for i, j, dim_x, dim_y in self._iter_subplots(): self.line_visual.add_batch_data( - pos=[[-1., 0., +1., 0.], - [0., -1., 0., +1.]], - color=(.5, .5, .5, .5), + pos=[[-1.0, 0.0, +1.0, 0.0], [0.0, -1.0, 0.0, +1.0]], + color=(0.5, 0.5, 0.5, 0.5), box_index=(i, j), data_bounds=None, ) @@ -282,7 +281,10 @@ def _plot_axes(self): def _get_lim(self, bunchs): if not bunchs: # pragma: no cover return 1 - m, M = min(bunch.data.min() for bunch in bunchs), max(bunch.data.max() for bunch in bunchs) + m, M = ( + min(bunch.data.min() for bunch in bunchs), + max(bunch.data.max() for bunch in bunchs), + ) M = max(abs(m), abs(M)) return M @@ -306,7 +308,9 @@ def get_clusters_data(self, fixed_channels=None, load_all=None): # Specify the channel ids if these are fixed, otherwise # choose the first cluster's best channels. c = self.channel_ids if fixed_channels else None - bunchs = [self.features(cluster_id, channel_ids=c) for cluster_id in self.cluster_ids] + bunchs = [ + self.features(cluster_id, channel_ids=c) for cluster_id in self.cluster_ids + ] bunchs = [b for b in bunchs if b] if not bunchs: # pragma: no cover return [] @@ -318,26 +322,32 @@ def get_clusters_data(self, fixed_channels=None, load_all=None): common_channels = list(channel_ids) # Intersection (with order kept) of channels belonging to all clusters. for bunch in bunchs: - common_channels = [c for c in bunch.get('channel_ids', []) if c in common_channels] + common_channels = [ + c for c in bunch.get('channel_ids', []) if c in common_channels + ] # The selected channels will be (1) the channels common to all clusters, followed # by (2) remaining channels from the first cluster (excluding those already selected # in (1)). n = len(channel_ids) not_common_channels = [c for c in channel_ids if c not in common_channels] - channel_ids = common_channels + not_common_channels[:n - len(common_channels)] + channel_ids = common_channels + not_common_channels[: n - len(common_channels)] assert len(channel_ids) > 0 # Choose the channels automatically unless fixed_channels is set. - if (not fixed_channels or self.channel_ids is None): + if not fixed_channels or self.channel_ids is None: self.channel_ids = channel_ids assert len(self.channel_ids) # Channel labels. self.channel_labels = {} for d in bunchs: - chl = d.get('channel_labels', ['%d' % ch for ch in d.get('channel_ids', [])]) - self.channel_labels.update({ - channel_id: chl[i] for i, channel_id in enumerate(d.get('channel_ids', []))}) + chl = d.get('channel_labels', [f'{ch}' for ch in d.get('channel_ids', [])]) + self.channel_labels.update( + { + channel_id: chl[i] + for i, channel_id in enumerate(d.get('channel_ids', [])) + } + ) return bunchs @@ -349,7 +359,8 @@ def plot(self, **kwargs): # Fix the channels if the view updates after a cluster event # and there are new clusters. fixed_channels = ( - self.fixed_channels or kwargs.get('fixed_channels', None) or added is not None) + self.fixed_channels or kwargs.get('fixed_channels') or added is not None + ) # Get the clusters data. bunchs = self.get_clusters_data(fixed_channels=fixed_channels) @@ -385,11 +396,13 @@ def plot(self, **kwargs): def attach(self, gui): """Attach the view to the GUI.""" - super(FeatureView, self).attach(gui) + super().attach(gui) self.actions.add( self.toggle_automatic_channel_selection, - checked=not self.fixed_channels, checkable=True) + checked=not self.fixed_channels, + checkable=True, + ) self.actions.add(self.clear_channels) self.actions.separator() @@ -402,7 +415,7 @@ def status(self): if self.channel_ids is None: # pragma: no cover return '' channel_labels = [self.channel_labels[ch] for ch in self.channel_ids[:2]] - return 'channels: %s' % ', '.join(channel_labels) + return f'channels: {", ".join(channel_labels)}' # Dimension selection # ------------------------------------------------------------------------- @@ -434,7 +447,7 @@ def on_select_channel(self, sender=None, channel_id=None, key=None, button=None) assert channels[0] != channels[1] # Remove duplicate channels. self.channel_ids = _uniq(channels) - logger.debug("Choose channels %d and %d in feature view.", *channels[:2]) + logger.debug('Choose channels %d and %d in feature view.', *channels[:2]) # Fix the channels temporarily. self.plot(fixed_channels=True) self.update_status() @@ -455,19 +468,24 @@ def on_mouse_click(self, e): if channel_pc is None: return channel_id, pc = channel_pc - logger.debug("Click on feature dim %s, channel id %s, PC %s.", dim, channel_id, pc) + logger.debug( + 'Click on feature dim %s, channel id %s, PC %s.', + dim, + channel_id, + pc, + ) else: # When the selected dimension is an attribute, e.g. "time". pc = None # Take the channel id in the other dimension. channel_pc = self._get_channel_and_pc(other_dim) channel_id = channel_pc[0] if channel_pc is not None else None - logger.debug("Click on feature dim %s.", dim) + logger.debug('Click on feature dim %s.', dim) emit('select_feature', self, dim=dim, channel_id=channel_id, pc=pc) def on_request_split(self, sender=None): """Return the spikes enclosed by the lasso.""" - if (self.canvas.lasso.count < 3 or not len(self.cluster_ids)): # pragma: no cover + if self.canvas.lasso.count < 3 or not len(self.cluster_ids): # pragma: no cover return np.array([], dtype=np.int64) assert len(self.channel_ids) @@ -482,7 +500,9 @@ def on_request_split(self, sender=None): for cluster_id in self.cluster_ids: # Load all spikes. - bunch = self.features(cluster_id, channel_ids=self.channel_ids, load_all=True) + bunch = self.features( + cluster_id, channel_ids=self.channel_ids, load_all=True + ) if not bunch: continue px = self._get_axis_data(bunch, dim_x, cluster_id=cluster_id, load_all=True) diff --git a/phy/cluster/views/histogram.py b/phy/cluster/views/histogram.py index 6d83e4496..63ed27b4e 100644 --- a/phy/cluster/views/histogram.py +++ b/phy/cluster/views/histogram.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """Histogram view.""" @@ -10,10 +8,11 @@ import logging import numpy as np - from phylib.io.array import _clip + from phy.plot.visuals import HistogramVisual, TextVisual from phy.utils.color import selected_cluster_color + from .base import ManualClusteringView, ScalingMixin logger = logging.getLogger(__name__) @@ -23,8 +22,10 @@ # Histogram view # ----------------------------------------------------------------------------- + def _compute_histogram( - data, x_max=None, x_min=None, n_bins=None, normalize=True, ignore_zeros=False): + data, x_max=None, x_min=None, n_bins=None, normalize=True, ignore_zeros=False +): """Compute the histogram of an array.""" assert x_min <= x_max assert n_bins >= 0 @@ -37,7 +38,7 @@ def _compute_histogram( return histogram # Normalize by the integral of the histogram. hist_sum = histogram.sum() * (bins[1] - bins[0]) - return histogram / (hist_sum or 1.) + return histogram / (hist_sum or 1.0) def _first_not_null(*l): @@ -68,7 +69,7 @@ class HistogramView(ScalingMixin, ManualClusteringView): n_bins = 100 # Step on the x axis when changing the histogram range with the mouse wheel. - x_delta = .01 # in seconds + x_delta = 0.01 # in seconds # Minimum value on the x axis (determines the range of the histogram) # If None, then `data.min()` is used. @@ -90,17 +91,17 @@ class HistogramView(ScalingMixin, ManualClusteringView): } default_snippets = { - 'set_n_bins': '%sn' % alias_char, - 'set_bin_size (%s)' % bin_unit: '%sb' % alias_char, - 'set_x_min (%s)' % bin_unit: '%smin' % alias_char, - 'set_x_max (%s)' % bin_unit: '%smax' % alias_char, + 'set_n_bins': f'{alias_char}n', + f'set_bin_size ({bin_unit})': f'{alias_char}b', + f'set_x_min ({bin_unit})': f'{alias_char}min', + f'set_x_max ({bin_unit})': f'{alias_char}max', } _state_attrs = ('n_bins', 'x_min', 'x_max') _local_state_attrs = () def __init__(self, cluster_stat=None): - super(HistogramView, self).__init__() + super().__init__() self.state_attrs += self._state_attrs self.local_state_attrs += self._local_state_attrs self.canvas.set_layout(layout='stacked', n_plots=1) @@ -114,7 +115,7 @@ def __init__(self, cluster_stat=None): # self.plot_visual = PlotVisual() # self.canvas.add_visual(self.plot_visual) - self.text_visual = TextVisual(color=(1., 1., 1., 1.)) + self.text_visual = TextVisual(color=(1.0, 1.0, 1.0, 1.0)) self.canvas.add_visual(self.text_visual) def _plot_cluster(self, bunch): @@ -124,7 +125,8 @@ def _plot_cluster(self, bunch): # Update the visual's data. self.visual.add_batch_data( - hist=bunch.histogram, ylim=bunch.ylim, color=bunch.color, box_index=bunch.index) + hist=bunch.histogram, ylim=bunch.ylim, color=bunch.color, box_index=bunch.index + ) # # Plot. # plot = bunch.get('plot', None) @@ -142,7 +144,9 @@ def _plot_cluster(self, bunch): text = text.splitlines() n = len(text) self.text_visual.add_batch_data( - text=text, pos=[(-1, .8)] * n, anchor=[(1, -1 - 2 * i) for i in range(n)], + text=text, + pos=[(-1, 0.8)] * n, + anchor=[(1, -1 - 2 * i) for i in range(n)], box_index=bunch.index, ) @@ -163,7 +167,8 @@ def get_clusters_data(self, load_all=None): # Compute the histogram. bunch.histogram = _compute_histogram( - bunch.data, x_min=self.x_min, x_max=self.x_max, n_bins=self.n_bins) + bunch.data, x_min=self.x_min, x_max=self.x_max, n_bins=self.n_bins + ) bunch.ylim = bunch.histogram.max() bunch.color = selected_cluster_color(i) @@ -199,27 +204,38 @@ def plot(self, **kwargs): def attach(self, gui): """Attach the view to the GUI.""" - super(HistogramView, self).attach(gui) + super().attach(gui) self.actions.add( - self.set_n_bins, alias=self.alias_char + 'n', - prompt=True, prompt_default=lambda: self.n_bins) + self.set_n_bins, + alias=f'{self.alias_char}n', + prompt=True, + prompt_default=lambda: self.n_bins, + ) self.actions.add( - self.set_bin_size, alias=self.alias_char + 'b', - prompt=True, prompt_default=lambda: self.bin_size) + self.set_bin_size, + alias=f'{self.alias_char}b', + prompt=True, + prompt_default=lambda: self.bin_size, + ) self.actions.add( - self.set_x_min, alias=self.alias_char + 'min', - prompt=True, prompt_default=lambda: self.x_min) + self.set_x_min, + alias=f'{self.alias_char}min', + prompt=True, + prompt_default=lambda: self.x_min, + ) self.actions.add( - self.set_x_max, alias=self.alias_char + 'max', - prompt=True, prompt_default=lambda: self.x_max) + self.set_x_max, + alias=f'{self.alias_char}max', + prompt=True, + prompt_default=lambda: self.x_max, + ) self.actions.separator() @property def status(self): f = 1 if self.bin_unit == 's' else 1000 - return '[{:.1f}{u}, {:.1f}{u:s}]'.format( - (self.x_min or 0) * f, (self.x_max or 0) * f, u=self.bin_unit) + return f'[{(self.x_min or 0) * f:.1f}{self.bin_unit}, {(self.x_max or 0) * f:.1f}{self.bin_unit:s}]' # Histogram parameters # ------------------------------------------------------------------------- @@ -235,7 +251,7 @@ def _set_scaling_value(self, value): def set_n_bins(self, n_bins): """Set the number of bins in the histogram.""" self.n_bins = n_bins - logger.debug("Change number of bins to %d for %s.", n_bins, self.__class__.__name__) + logger.debug('Change number of bins to %d for %s.', n_bins, self.__class__.__name__) self.plot() @property @@ -252,7 +268,7 @@ def set_bin_size(self, bin_size): if self.bin_unit == 'ms': bin_size /= 1000 self.n_bins = np.round((self.x_max - self.x_min) / bin_size) - logger.debug("Change number of bins to %d for %s.", self.n_bins, self.__class__.__name__) + logger.debug('Change number of bins to %d for %s.', self.n_bins, self.__class__.__name__) self.plot() def set_x_min(self, x_min): @@ -263,7 +279,7 @@ def set_x_min(self, x_min): if x_min == self.x_max: return self.x_min = x_min - logger.log(5, "Change x min to %s for %s.", x_min, self.__class__.__name__) + logger.log(5, 'Change x min to %s for %s.', x_min, self.__class__.__name__) self.plot() def set_x_max(self, x_max): @@ -274,19 +290,19 @@ def set_x_max(self, x_max): if x_max == self.x_min: return self.x_max = x_max - logger.log(5, "Change x max to %s for %s.", x_max, self.__class__.__name__) + logger.log(5, 'Change x max to %s for %s.', x_max, self.__class__.__name__) self.plot() def on_mouse_wheel(self, e): # pragma: no cover """Change the scaling with the wheel.""" - super(HistogramView, self).on_mouse_wheel(e) + super().on_mouse_wheel(e) if e.modifiers == ('Shift',): - self.x_min *= 1.1 ** e.delta + self.x_min *= 1.1**e.delta self.x_min = min(self.x_min, self.x_max) if self.x_min < self.x_max: self.plot() elif e.modifiers == ('Alt',): - self.n_bins /= 1.05 ** e.delta + self.n_bins /= 1.05**e.delta self.n_bins = int(self.n_bins) self.n_bins = max(2, self.n_bins) self.plot() @@ -294,9 +310,10 @@ def on_mouse_wheel(self, e): # pragma: no cover class ISIView(HistogramView): """Histogram view showing the interspike intervals.""" + x_min = 0 - x_max = .05 # window size is 50 ms by default - n_bins = int(x_max / .001) # by default, 1 bin = 1 ms + x_max = 0.05 # window size is 50 ms by default + n_bins = int(x_max / 0.001) # by default, 1 bin = 1 ms alias_char = 'isi' # provide `:isisn` (set number of bins) and `:isim` (set max bin) snippets bin_unit = 'ms' # user-provided bin values in milliseconds, but stored in seconds @@ -305,15 +322,16 @@ class ISIView(HistogramView): } default_snippets = { - 'set_n_bins': '%sn' % alias_char, - 'set_bin_size (%s)' % bin_unit: '%sb' % alias_char, - 'set_x_min (%s)' % bin_unit: '%smin' % alias_char, - 'set_x_max (%s)' % bin_unit: '%smax' % alias_char, + 'set_n_bins': f'{alias_char}n', + f'set_bin_size ({bin_unit})': f'{alias_char}b', + f'set_x_min ({bin_unit})': f'{alias_char}min', + f'set_x_max ({bin_unit})': f'{alias_char}max', } class FiringRateView(HistogramView): """Histogram view showing the time-dependent firing rate.""" + n_bins = 200 alias_char = 'fr' bin_unit = 's' @@ -327,8 +345,8 @@ class FiringRateView(HistogramView): } default_snippets = { - 'set_n_bins': '%sn' % alias_char, - 'set_bin_size (%s)' % bin_unit: '%sb' % alias_char, - 'set_x_min (%s)' % bin_unit: '%smin' % alias_char, - 'set_x_max (%s)' % bin_unit: '%smax' % alias_char, + 'set_n_bins': f'{alias_char}n', + f'set_bin_size ({bin_unit})': f'{alias_char}b', + f'set_x_min ({bin_unit})': f'{alias_char}min', + f'set_x_max ({bin_unit})': f'{alias_char}max', } diff --git a/phy/cluster/views/probe.py b/phy/cluster/views/probe.py index 43d535e24..87cf4c0a7 100644 --- a/phy/cluster/views/probe.py +++ b/phy/cluster/views/probe.py @@ -1,19 +1,18 @@ -# -*- coding: utf-8 -*- - """Probe view.""" # ----------------------------------------------------------------------------- # Imports # ----------------------------------------------------------------------------- -from collections import defaultdict import logging +from collections import defaultdict import numpy as np - -from phy.utils.color import selected_cluster_color from phylib.utils.geometry import get_non_overlapping_boxes + from phy.plot.visuals import ScatterVisual, TextVisual +from phy.utils.color import selected_cluster_color + from .base import ManualClusteringView logger = logging.getLogger(__name__) @@ -23,13 +22,14 @@ # Probe view # ----------------------------------------------------------------------------- + def _get_pos_data_bounds(positions): positions, _ = get_non_overlapping_boxes(positions) x, y = positions.T xmin, ymin, xmax, ymax = x.min(), y.min(), x.max(), y.max() w = xmax - xmin h = ymax - ymin - k = .05 + k = 0.05 data_bounds = (xmin - w * k, ymin - h * k, xmax + w * k, ymax + h * k) return positions, data_bounds @@ -64,14 +64,14 @@ class ProbeView(ManualClusteringView): selected_marker_size = 15 # Alpha value of the dead channels. - dead_channel_alpha = .25 + dead_channel_alpha = 0.25 do_show_labels = False def __init__( - self, positions=None, best_channels=None, channel_labels=None, - dead_channels=None, **kwargs): - super(ProbeView, self).__init__(**kwargs) + self, positions=None, best_channels=None, channel_labels=None, dead_channels=None, **kwargs + ): + super().__init__(**kwargs) self.state_attrs += ('do_show_labels',) # Normalize positions. @@ -91,13 +91,16 @@ def __init__( # Probe visual. color = np.ones((self.n_channels, 4)) - color[:, :3] = .5 + color[:, :3] = 0.5 # Change alpha value for dead channels. if len(self.dead_channels): color[self.dead_channels, 3] = self.dead_channel_alpha self.probe_visual.set_data( - pos=self.positions, data_bounds=self.data_bounds, - color=color, size=self.unselected_marker_size) + pos=self.positions, + data_bounds=self.data_bounds, + color=color, + size=self.unselected_marker_size, + ) # Cluster visual. self.cluster_visual = ScatterVisual() @@ -109,14 +112,19 @@ def __init__( self.text_visual = TextVisual() self.text_visual.inserter.insert_vert('uniform float n_channels;', 'header') self.text_visual.inserter.add_varying( - 'float', 'v_discard', + 'float', + 'v_discard', 'float((n_channels >= 200 * u_zoom.y) && ' - '(mod(int(a_string_index), int(n_channels / (200 * u_zoom.y))) >= 1))') + '(mod(int(a_string_index), int(n_channels / (200 * u_zoom.y))) >= 1))', + ) self.text_visual.inserter.insert_frag('if (v_discard > 0) discard;', 'end') self.canvas.add_visual(self.text_visual) self.text_visual.set_data( - pos=self.positions, text=self.channel_labels, anchor=[0, -1], - data_bounds=self.data_bounds, color=color + pos=self.positions, + text=self.channel_labels, + anchor=[0, -1], + data_bounds=self.data_bounds, + color=color, ) self.text_visual.program['n_channels'] = self.n_channels self.canvas.update() @@ -128,7 +136,7 @@ def _get_clu_positions(self, cluster_ids): cluster_channels = {i: self.best_channels(cl) for i, cl in enumerate(cluster_ids)} # List of clusters per channel. - clusters_per_channel = defaultdict(lambda: []) + clusters_per_channel = defaultdict(list) for clu_idx, channels in cluster_channels.items(): for channel in channels: clusters_per_channel[channel].append(clu_idx) @@ -141,7 +149,7 @@ def _get_clu_positions(self, cluster_ids): for i, clu_idx in enumerate(clusters_per_channel[channel_id]): n = len(clusters_per_channel[channel_id]) # Translation. - t = .025 * w * (i - .5 * (n - 1)) + t = 0.025 * w * (i - 0.5 * (n - 1)) x += t alpha = 1.0 if channel_id not in self.dead_channels else self.dead_channel_alpha clu_pos.append((x, y)) @@ -155,12 +163,13 @@ def on_select(self, cluster_ids=(), **kwargs): return pos, colors = self._get_clu_positions(cluster_ids) self.cluster_visual.set_data( - pos=pos, color=colors, size=self.selected_marker_size, data_bounds=self.data_bounds) + pos=pos, color=colors, size=self.selected_marker_size, data_bounds=self.data_bounds + ) self.canvas.update() def attach(self, gui): """Attach the view to the GUI.""" - super(ProbeView, self).attach(gui) + super().attach(gui) self.actions.add(self.toggle_show_labels, checkable=True, checked=self.do_show_labels) if not self.do_show_labels: @@ -168,7 +177,7 @@ def attach(self, gui): def toggle_show_labels(self, checked): """Toggle the display of the channel ids.""" - logger.debug("Set show labels to %s.", checked) + logger.debug('Set show labels to %s.', checked) self.do_show_labels = checked self.text_visual._hidden = not checked self.canvas.update() diff --git a/phy/cluster/views/raster.py b/phy/cluster/views/raster.py index 54f62fe1a..9a1b00aa0 100644 --- a/phy/cluster/views/raster.py +++ b/phy/cluster/views/raster.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """Scatter view.""" @@ -10,13 +8,13 @@ import logging import numpy as np - from phylib.io.array import _index_of from phylib.utils import emit -from phy.utils.color import _add_selected_clusters_colors -from .base import ManualClusteringView, BaseGlobalView, MarkerSizeMixin, BaseColorView from phy.plot.visuals import ScatterVisual +from phy.utils.color import _add_selected_clusters_colors + +from .base import BaseColorView, BaseGlobalView, ManualClusteringView, MarkerSizeMixin logger = logging.getLogger(__name__) @@ -25,6 +23,7 @@ # Raster view # ----------------------------------------------------------------------------- + class RasterView(MarkerSizeMixin, BaseColorView, BaseGlobalView, ManualClusteringView): """This view shows a raster plot of all clusters. @@ -61,24 +60,27 @@ def __init__(self, spike_times, spike_clusters, cluster_ids=None, **kwargs): self.set_spike_clusters(spike_clusters) self.set_cluster_ids(cluster_ids) - super(RasterView, self).__init__(**kwargs) + super().__init__(**kwargs) self.canvas.set_layout('stacked', origin='top', n_plots=self.n_clusters, has_clip=False) self.canvas.enable_axes() self.visual = ScatterVisual( marker='vbar', - marker_scaling=''' + marker_scaling=""" point_size = v_size * u_zoom.y + 5.; float width = 0.2; float height = 0.5; vec2 marker_size = point_size * vec2(width, height); marker_size.x = clamp(marker_size.x, 1, 20); - ''', + """, ) - self.visual.inserter.insert_vert(''' + self.visual.inserter.insert_vert( + """ gl_PointSize = a_size * u_zoom.y + 5.0; - ''', 'end') + """, + 'end', + ) self.canvas.add_visual(self.visual) self.canvas.panzoom.set_constrain_bounds((-1, -2, +1, +2)) @@ -119,11 +121,12 @@ def _get_box_index(self): def _get_color(self, box_index, selected_clusters=None): """Return, for every spike, its color, based on its box index.""" - cluster_colors = self.get_cluster_colors(self.all_cluster_ids, alpha=.75) + cluster_colors = self.get_cluster_colors(self.all_cluster_ids, alpha=0.75) # Selected cluster colors. if selected_clusters is not None: cluster_colors = _add_selected_clusters_colors( - selected_clusters, self.all_cluster_ids, cluster_colors) + selected_clusters, self.all_cluster_ids, cluster_colors + ) return cluster_colors[box_index, :] # Main methods @@ -148,7 +151,7 @@ def update_color(self): @property def status(self): - return 'Color scheme: %s' % self.color_scheme + return f'Color scheme: {self.color_scheme}' def plot(self, **kwargs): """Make the raster plot.""" @@ -163,8 +166,8 @@ def plot(self, **kwargs): self.data_bounds = self._get_data_bounds() self.visual.set_data( - x=x, y=y, color=color, size=self.marker_size, - data_bounds=(0, -1, self.duration, 1)) + x=x, y=y, color=color, size=self.marker_size, data_bounds=(0, -1, self.duration, 1) + ) self.visual.set_box_index(box_index) self.canvas.stacked.n_boxes = self.n_clusters self._update_axes() @@ -173,14 +176,14 @@ def plot(self, **kwargs): def attach(self, gui): """Attach the view to the GUI.""" - super(RasterView, self).attach(gui) + super().attach(gui) self.actions.add(self.increase_marker_size) self.actions.add(self.decrease_marker_size) self.actions.separator() def on_select(self, *args, **kwargs): - super(RasterView, self).on_select(*args, **kwargs) + super().on_select(*args, **kwargs) self.update_color() def zoom_to_time_range(self, interval): @@ -188,8 +191,8 @@ def zoom_to_time_range(self, interval): if not interval: return t0, t1 = interval - w = .5 * (t1 - t0) # half width - tm = .5 * (t0 + t1) + w = 0.5 * (t1 - t0) # half width + tm = 0.5 * (t0 + t1) w = min(5, w) # minimum 5s time range t0, t1 = tm - w, tm + w x0 = -1 + 2 * t0 / self.duration @@ -208,7 +211,7 @@ def on_mouse_click(self, e): # Get mouse position in NDC. cluster_idx, _ = self.canvas.stacked.box_map(e.pos) cluster_id = self.all_cluster_ids[cluster_idx] - logger.debug("Click on cluster %d with button %s.", cluster_id, b) + logger.debug('Click on cluster %d with button %s.', cluster_id, b) if 'Shift' in e.modifiers: emit('select_more', self, [cluster_id]) else: diff --git a/phy/cluster/views/scatter.py b/phy/cluster/views/scatter.py index 948e0fd12..9d5924e37 100644 --- a/phy/cluster/views/scatter.py +++ b/phy/cluster/views/scatter.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """Scatter view.""" @@ -12,9 +10,10 @@ import numpy as np -from phy.utils.color import selected_cluster_color, spike_colors -from .base import ManualClusteringView, MarkerSizeMixin, LassoMixin from phy.plot.visuals import ScatterVisual +from phy.utils.color import selected_cluster_color, spike_colors + +from .base import LassoMixin, ManualClusteringView, MarkerSizeMixin logger = logging.getLogger(__name__) @@ -23,6 +22,7 @@ # Scatter view # ----------------------------------------------------------------------------- + class ScatterView(MarkerSizeMixin, LassoMixin, ManualClusteringView): """This view displays a scatter plot for all selected clusters. @@ -44,7 +44,7 @@ class ScatterView(MarkerSizeMixin, LassoMixin, ManualClusteringView): } def __init__(self, coords=None, **kwargs): - super(ScatterView, self).__init__(**kwargs) + super().__init__(**kwargs) # Save the marker size in the global and local view's config. self.canvas.enable_axes() @@ -57,7 +57,8 @@ def __init__(self, coords=None, **kwargs): def _plot_cluster(self, bunch): ms = self._marker_size self.visual.add_batch_data( - pos=bunch.pos, color=bunch.color, size=ms, data_bounds=self.data_bounds) + pos=bunch.pos, color=bunch.color, size=ms, data_bounds=self.data_bounds + ) def _get_split_cluster_data(self, bunchs): """Get the data when there is one Bunch per cluster.""" @@ -70,7 +71,7 @@ def _get_split_cluster_data(self, bunchs): bunch.pos = np.c_[bunch.x, bunch.y] assert bunch.pos.ndim == 2 assert 'spike_ids' in bunch - bunch.color = selected_cluster_color(i, .75) + bunch.color = selected_cluster_color(i, 0.75) return bunchs def _get_collated_cluster_data(self, bunch): @@ -92,14 +93,15 @@ def get_clusters_data(self, load_all=None): bunchs = self.coords(self.cluster_ids, load_all=load_all) or () else: logger.warning( - "The view `%s` may not load all spikes when using the lasso for splitting.", - self.__class__.__name__) + 'The view `%s` may not load all spikes when using the lasso for splitting.', + self.__class__.__name__, + ) bunchs = self.coords(self.cluster_ids) if isinstance(bunchs, dict): return [self._get_collated_cluster_data(bunchs)] elif isinstance(bunchs, (list, tuple)): return self._get_split_cluster_data(bunchs) - raise ValueError("The output of `coords()` should be either a list of Bunch, or a Bunch.") + raise ValueError('The output of `coords()` should be either a list of Bunch, or a Bunch.') def plot(self, **kwargs): """Update the view with the current cluster selection.""" diff --git a/phy/cluster/views/template.py b/phy/cluster/views/template.py index b24441f1f..5ffb0a30c 100644 --- a/phy/cluster/views/template.py +++ b/phy/cluster/views/template.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """Template view.""" @@ -10,14 +8,14 @@ import logging import numpy as np - -from phy.utils.color import _add_selected_clusters_colors from phylib.io.array import _index_of -from phylib.utils import emit, Bunch +from phylib.utils import Bunch, emit from phy.plot import get_linear_x from phy.plot.visuals import PlotVisual -from .base import ManualClusteringView, BaseGlobalView, ScalingMixin, BaseColorView +from phy.utils.color import _add_selected_clusters_colors + +from .base import BaseColorView, BaseGlobalView, ManualClusteringView, ScalingMixin logger = logging.getLogger(__name__) @@ -26,6 +24,7 @@ # Template view # ----------------------------------------------------------------------------- + class TemplateView(ScalingMixin, BaseColorView, BaseGlobalView, ManualClusteringView): """This view shows all template waveforms of all clusters in a large grid of shape `(n_channels, n_clusters)`. @@ -45,8 +44,9 @@ class TemplateView(ScalingMixin, BaseColorView, BaseGlobalView, ManualClustering The list of all clusters to show initially. """ + _default_position = 'right' - _scaling = 1. + _scaling = 1.0 default_shortcuts = { 'change_template_size': 'ctrl+wheel', @@ -58,9 +58,14 @@ class TemplateView(ScalingMixin, BaseColorView, BaseGlobalView, ManualClustering } def __init__( - self, templates=None, channel_ids=None, channel_labels=None, - cluster_ids=None, **kwargs): - super(TemplateView, self).__init__(**kwargs) + self, + templates=None, + channel_ids=None, + channel_labels=None, + cluster_ids=None, + **kwargs, + ): + super().__init__(**kwargs) self.state_attrs += () self.local_state_attrs += ('scaling',) @@ -70,8 +75,10 @@ def __init__( # Channel labels. self.channel_labels = ( - channel_labels if channel_labels is not None else - ['%d' % ch for ch in range(self.n_channels)]) + channel_labels + if channel_labels is not None + else [f'{ch}' for ch in range(self.n_channels)] + ) assert len(self.channel_labels) == self.n_channels # TODO: show channel and cluster labels @@ -108,7 +115,8 @@ def _get_box_index(self, bunch): box_index = np.repeat(box_index, n_samples) box_index = np.c_[ box_index.reshape((-1, 1)), - bunch.cluster_idx * np.ones((n_samples * len(bunch.channel_ids), 1))] + bunch.cluster_idx * np.ones((n_samples * len(bunch.channel_ids), 1)), + ] assert box_index.shape == (len(bunch.channel_ids) * n_samples, 2) assert box_index.size == bunch.template.size * 2 return box_index @@ -131,7 +139,12 @@ def _plot_cluster(self, bunch, color=None): box_index = self._get_box_index(bunch) return Bunch( - x=t, y=wave.T, color=color, box_index=box_index, data_bounds=self.data_bounds) + x=t, + y=wave.T, + color=color, + box_index=box_index, + data_bounds=self.data_bounds, + ) def set_cluster_ids(self, cluster_ids): """Update the cluster ids when their identity or order has changed.""" @@ -142,7 +155,9 @@ def set_cluster_ids(self, cluster_ids): self.cluster_idxs = np.argsort(self.all_cluster_ids) self.sorted_cluster_ids = self.all_cluster_ids[self.cluster_idxs] # Cluster colors, ordered by cluster id. - self.cluster_colors = self.get_cluster_colors(self.sorted_cluster_ids, alpha=.75) + self.cluster_colors = self.get_cluster_colors( + self.sorted_cluster_ids, alpha=0.75 + ) def get_clusters_data(self, load_all=None): """Return all templates data.""" @@ -193,17 +208,20 @@ def update_color(self): selected_clusters = self.cluster_ids if selected_clusters is not None: cluster_colors = _add_selected_clusters_colors( - selected_clusters, self.sorted_cluster_ids, cluster_colors) + selected_clusters, self.sorted_cluster_ids, cluster_colors + ) # Number of vertices per cluster = number of vertices per signal n_vertices_clu = [ - len(self._cluster_box_index[cluster_id]) for cluster_id in self.sorted_cluster_ids] + len(self._cluster_box_index[cluster_id]) + for cluster_id in self.sorted_cluster_ids + ] # The argument passed to set_color() must have 1 row per vertex. self.visual.set_color(np.repeat(cluster_colors, n_vertices_clu, axis=0)) self.canvas.update() @property def status(self): - return 'Color scheme: %s' % self.color_schemes.current + return f'Color scheme: {self.color_schemes.current}' def plot(self, **kwargs): """Make the template plot.""" @@ -228,7 +246,7 @@ def plot(self, **kwargs): self.canvas.update() def on_select(self, *args, **kwargs): - super(TemplateView, self).on_select(*args, **kwargs) + super().on_select(*args, **kwargs) self.update_color() # Scaling @@ -262,7 +280,7 @@ def on_mouse_click(self, e): # Get mouse position in NDC. (channel_idx, cluster_rel), _ = self.canvas.grid.box_map(e.pos) cluster_id = self.all_cluster_ids[cluster_rel] - logger.debug("Click on cluster %d with button %s.", cluster_id, b) + logger.debug('Click on cluster %d with button %s.', cluster_id, b) if 'Shift' in e.modifiers: emit('select_more', self, [cluster_id]) else: diff --git a/phy/cluster/views/tests/conftest.py b/phy/cluster/views/tests/conftest.py index 4df26295e..7931681a8 100644 --- a/phy/cluster/views/tests/conftest.py +++ b/phy/cluster/views/tests/conftest.py @@ -1,19 +1,17 @@ -# -*- coding: utf-8 -*- - """Test cluster views.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ from pytest import fixture from phy.gui import GUI - -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Utilities and fixtures -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + @fixture def gui(tempdir, qtbot): @@ -21,8 +19,8 @@ def gui(tempdir, qtbot): gui.set_default_actions() gui.show() qtbot.wait(1) - #qtbot.addWidget(gui) - #qtbot.waitForWindowShown(gui) + # qtbot.addWidget(gui) + # qtbot.waitForWindowShown(gui) yield gui qtbot.wait(1) gui.close() diff --git a/phy/cluster/views/tests/test_amplitude.py b/phy/cluster/views/tests/test_amplitude.py index f2943de77..0e12f249a 100644 --- a/phy/cluster/views/tests/test_amplitude.py +++ b/phy/cluster/views/tests/test_amplitude.py @@ -1,24 +1,22 @@ -# -*- coding: utf-8 -*- - """Test amplitude view.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import numpy as np - from phylib.io.mock import artificial_spike_samples from phylib.utils import Bunch, connect from phy.plot.tests import mouse_click + from ..amplitude import AmplitudeView from . import _stop_and_close - -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test amplitude view -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_amplitude_view_0(qtbot, gui): v = AmplitudeView( @@ -40,35 +38,44 @@ def test_amplitude_view_1(qtbot, gui): x = np.zeros(1) v = AmplitudeView( amplitudes=lambda cluster_ids, load_all=False: [ - Bunch(amplitudes=x, spike_ids=[0], spike_times=[0])], + Bunch(amplitudes=x, spike_ids=[0], spike_times=[0]) + ], ) v.show() qtbot.waitForWindowShown(v.canvas) v.attach(gui) v.on_select(cluster_ids=[0]) - v.show_time_range((.499, .501)) + v.show_time_range((0.499, 0.501)) _stop_and_close(qtbot, v) def test_amplitude_view_2(qtbot, gui): n = 1000 - st1 = artificial_spike_samples(n) / 20000. - st2 = artificial_spike_samples(n) / 20000. + st1 = artificial_spike_samples(n) / 20000.0 + st2 = artificial_spike_samples(n) / 20000.0 v = AmplitudeView( amplitudes={ - 'amp1': lambda cluster_ids, load_all=False: [Bunch( - amplitudes=15 + np.random.randn(n), - spike_ids=np.arange(n), - spike_times=st1, - ) for c in cluster_ids], - 'amp2': lambda cluster_ids, load_all=False: [Bunch( - amplitudes=10 + np.random.randn(n), - spike_ids=np.arange(n), - spike_times=st2, - ) for c in cluster_ids], - }, duration=max(st1.max(), st2.max())) + 'amp1': lambda cluster_ids, load_all=False: [ + Bunch( + amplitudes=15 + np.random.randn(n), + spike_ids=np.arange(n), + spike_times=st1, + ) + for c in cluster_ids + ], + 'amp2': lambda cluster_ids, load_all=False: [ + Bunch( + amplitudes=10 + np.random.randn(n), + spike_ids=np.arange(n), + spike_times=st2, + ) + for c in cluster_ids + ], + }, + duration=max(st1.max(), st2.max()), + ) v.show() qtbot.waitForWindowShown(v.canvas) v.attach(gui) @@ -91,9 +98,10 @@ def test_amplitude_view_2(qtbot, gui): @connect(sender=v) def on_select_time(sender, time): _times.append(time) + mouse_click(qtbot, v.canvas, (w / 3, h / 2), modifiers=('Alt',)) assert len(_times) == 1 - assert np.allclose(_times[0], .5, atol=.01) + assert np.allclose(_times[0], 0.5, atol=0.01) # Split without selection. spike_ids = v.on_request_split() diff --git a/phy/cluster/views/tests/test_base.py b/phy/cluster/views/tests/test_base.py index 76b078f9d..0e8617cf0 100644 --- a/phy/cluster/views/tests/test_base.py +++ b/phy/cluster/views/tests/test_base.py @@ -1,27 +1,28 @@ -# -*- coding: utf-8 -*- - """Test scatter view.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import numpy as np - from phylib.utils import emit -from phy.utils.color import selected_cluster_color, colormaps + +from phy.utils.color import colormaps, selected_cluster_color + from ..base import BaseColorView, ManualClusteringView from . import _stop_and_close - -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test manual clustering view -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + class MyView(BaseColorView, ManualClusteringView): def plot(self, **kwargs): for i in range(len(self.cluster_ids)): - self.canvas.scatter(pos=.25 * np.random.randn(100, 2), color=selected_cluster_color(i)) + self.canvas.scatter( + pos=0.25 * np.random.randn(100, 2), color=selected_cluster_color(i) + ) @property def status(self): @@ -52,10 +53,11 @@ def test_manual_clustering_view_2(qtbot, gui): v = MyView() v.canvas.show() v.add_color_scheme( - lambda cid: cid, name='myscheme', colormap=colormaps.rainbow, cluster_ids=[0, 1]) + lambda cid: cid, name='myscheme', colormap=colormaps.rainbow, cluster_ids=[0, 1] + ) v.attach(gui) - class Supervisor(object): + class Supervisor: pass emit('select', Supervisor(), cluster_ids=[0, 1]) diff --git a/phy/cluster/views/tests/test_cluscatter.py b/phy/cluster/views/tests/test_cluscatter.py index 39891ad4e..7ba1ca095 100644 --- a/phy/cluster/views/tests/test_cluscatter.py +++ b/phy/cluster/views/tests/test_cluscatter.py @@ -1,47 +1,51 @@ -# -*- coding: utf-8 -*- - """Test views.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import numpy as np from numpy.random import RandomState - from phylib.utils import Bunch, connect, emit from phy.plot.tests import mouse_click + from ..cluscatter import ClusterScatterView from . import _stop_and_close - -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test cluster scatter view -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_cluster_scatter_view_1(qtbot, tempdir, gui): n_clusters = 1000 cluster_ids = np.arange(n_clusters)[2::3] - class Supervisor(object): + class Supervisor: pass + s = Supervisor() def cluster_info(cluster_id): np.random.seed(cluster_id) - return Bunch({ - 'fet1': np.random.randn(), - 'fet2': np.random.randn(), - 'fet3': np.random.uniform(low=5, high=20) - }) + return Bunch( + { + 'fet1': np.random.randn(), + 'fet2': np.random.randn(), + 'fet3': np.random.uniform(low=5, high=20), + } + ) bindings = Bunch({'x_axis': 'fet1', 'y_axis': 'fet2', 'size': 'fet3'}) v = ClusterScatterView(cluster_info=cluster_info, cluster_ids=cluster_ids, bindings=bindings) v.add_color_scheme( - lambda cluster_id: RandomState(cluster_id).rand(), name='depth', - colormap='linear', cluster_ids=cluster_ids) + lambda cluster_id: RandomState(cluster_id).rand(), + name='depth', + colormap='linear', + cluster_ids=cluster_ids, + ) v.show() v.plot() v.color_scheme = 'depth' @@ -91,14 +95,13 @@ def on_select_more(sender, cluster_ids): # noqa mouse_click(qtbot, v.canvas, pos=(w / 2, h / 2), button='Left', modifiers=()) assert len(_clicked) == 1 - mouse_click( - qtbot, v.canvas, pos=(w / 2, h / 2), button='Left', modifiers=('Shift',)) + mouse_click(qtbot, v.canvas, pos=(w / 2, h / 2), button='Left', modifiers=('Shift',)) assert len(_clicked) == 2 - mouse_click(qtbot, v.canvas, pos=(w * .3, h * .3), button='Left', modifiers=('Control',)) - mouse_click(qtbot, v.canvas, pos=(w * .7, h * .3), button='Left', modifiers=('Control',)) - mouse_click(qtbot, v.canvas, pos=(w * .7, h * .7), button='Left', modifiers=('Control',)) - mouse_click(qtbot, v.canvas, pos=(w * .3, h * .7), button='Left', modifiers=('Control',)) + mouse_click(qtbot, v.canvas, pos=(w * 0.3, h * 0.3), button='Left', modifiers=('Control',)) + mouse_click(qtbot, v.canvas, pos=(w * 0.7, h * 0.3), button='Left', modifiers=('Control',)) + mouse_click(qtbot, v.canvas, pos=(w * 0.7, h * 0.7), button='Left', modifiers=('Control',)) + mouse_click(qtbot, v.canvas, pos=(w * 0.3, h * 0.7), button='Left', modifiers=('Control',)) assert len(v.cluster_ids) >= 1 diff --git a/phy/cluster/views/tests/test_correlogram.py b/phy/cluster/views/tests/test_correlogram.py index c664c0294..4e0f9142b 100644 --- a/phy/cluster/views/tests/test_correlogram.py +++ b/phy/cluster/views/tests/test_correlogram.py @@ -1,35 +1,32 @@ -# -*- coding: utf-8 -*- - """Test correlogram view.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import numpy as np - from phylib.io.mock import artificial_correlograms from ..correlogram import CorrelogramView from . import _stop_and_close - -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test correlogram view -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ -def test_correlogram_view(qtbot, gui): +def test_correlogram_view(qtbot, gui): def get_correlograms(cluster_ids, bin_size, window_size): return artificial_correlograms(len(cluster_ids), int(window_size / bin_size)) def get_firing_rate(cluster_ids, bin_size): - return .5 * np.ones((len(cluster_ids), len(cluster_ids))) + return 0.5 * np.ones((len(cluster_ids), len(cluster_ids))) - v = CorrelogramView(correlograms=get_correlograms, - firing_rate=get_firing_rate, - sample_rate=100., - ) + v = CorrelogramView( + correlograms=get_correlograms, + firing_rate=get_firing_rate, + sample_rate=100.0, + ) v.show() qtbot.waitForWindowShown(v.canvas) v.attach(gui) @@ -47,8 +44,8 @@ def get_firing_rate(cluster_ids, bin_size): v.set_window(100) v.set_refractory_period(3) - assert v.bin_size == .001 - assert v.window_size == .1 + assert v.bin_size == 0.001 + assert v.window_size == 0.1 assert v.refractory_period == 3e-3 v.increase() diff --git a/phy/cluster/views/tests/test_feature.py b/phy/cluster/views/tests/test_feature.py index 13dd4f87d..5ecf163e8 100644 --- a/phy/cluster/views/tests/test_feature.py +++ b/phy/cluster/views/tests/test_feature.py @@ -1,26 +1,24 @@ -# -*- coding: utf-8 -*- - """Test views.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import numpy as np import pytest - from phylib.io.array import _spikes_per_cluster from phylib.io.mock import artificial_features, artificial_spike_clusters from phylib.utils import Bunch, connect + from phy.plot.tests import mouse_click from ..feature import FeatureView, _get_default_grid from . import _stop_and_close - -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test feature view -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + @pytest.mark.parametrize('n_channels', [5, 1]) def test_feature_view(qtbot, gui, n_channels): @@ -28,17 +26,14 @@ def test_feature_view(qtbot, gui, n_channels): ns = 10000 features = artificial_features(ns, nc, 4) spike_clusters = artificial_spike_clusters(ns, 4) - spike_times = np.linspace(0., 1., ns) + spike_times = np.linspace(0.0, 1.0, ns) spc = _spikes_per_cluster(spike_clusters) def get_spike_ids(cluster_id): - return (spc[cluster_id] if cluster_id is not None else np.arange(ns)) + return spc[cluster_id] if cluster_id is not None else np.arange(ns) def get_features(cluster_id=None, channel_ids=None, spike_ids=None, load_all=None): - if load_all: - spike_ids = spc[cluster_id] - else: - spike_ids = get_spike_ids(cluster_id) + spike_ids = spc[cluster_id] if load_all else get_spike_ids(cluster_id) return Bunch( data=features[spike_ids], spike_ids=spike_ids, @@ -47,7 +42,7 @@ def get_features(cluster_id=None, channel_ids=None, spike_ids=None, load_all=Non ) def get_time(cluster_id=None, load_all=None): - return Bunch(data=spike_times[get_spike_ids(cluster_id)], lim=(0., 1.)) + return Bunch(data=spike_times[get_spike_ids(cluster_id)], lim=(0.0, 1.0)) v = FeatureView(features=get_features, attributes={'time': get_time}) v.show() diff --git a/phy/cluster/views/tests/test_histogram.py b/phy/cluster/views/tests/test_histogram.py index bd8da6e12..e0b742dd0 100644 --- a/phy/cluster/views/tests/test_histogram.py +++ b/phy/cluster/views/tests/test_histogram.py @@ -1,21 +1,19 @@ -# -*- coding: utf-8 -*- - """Test Histogram view.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import numpy as np - from phylib.utils import Bunch + from ..histogram import HistogramView from . import _stop_and_close - -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test Histogram view -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_histogram_view_0(qtbot, gui): data = np.random.uniform(low=0, high=10, size=5000) @@ -24,7 +22,7 @@ def test_histogram_view_0(qtbot, gui): cluster_stat=lambda cluster_id: Bunch( data=data, # plot=plot, - text='this is:\ncluster %d' % cluster_id, + text=f'this is:\ncluster {cluster_id}', ) ) v.show() @@ -58,9 +56,9 @@ def test_histogram_view_0(qtbot, gui): # Use ms unit. v.bin_unit = 'ms' v.set_x_min(100) - assert v.x_min == .1 + assert v.x_min == 0.1 v.set_x_max(500) - assert v.x_max == .5 + assert v.x_max == 0.5 v.set_n_bins(400) assert v.bin_size == 1 # 1 ms v.set_bin_size(2) diff --git a/phy/cluster/views/tests/test_probe.py b/phy/cluster/views/tests/test_probe.py index 3a63c5cee..f3e502e20 100644 --- a/phy/cluster/views/tests/test_probe.py +++ b/phy/cluster/views/tests/test_probe.py @@ -1,26 +1,22 @@ -# -*- coding: utf-8 -*- - """Test probe view.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import numpy as np - -from phylib.utils.geometry import staggered_positions from phylib.utils import emit +from phylib.utils.geometry import staggered_positions from ..probe import ProbeView from . import _stop_and_close - -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test correlogram view -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ -def test_probe_view(qtbot, gui): +def test_probe_view(qtbot, gui): n = 50 positions = staggered_positions(n) positions = positions.astype(np.int32) @@ -32,7 +28,7 @@ def test_probe_view(qtbot, gui): qtbot.waitForWindowShown(v.canvas) v.attach(gui) - class Supervisor(object): + class Supervisor: pass v.toggle_show_labels(True) diff --git a/phy/cluster/views/tests/test_raster.py b/phy/cluster/views/tests/test_raster.py index 9b03f4ae1..ac664a4b7 100644 --- a/phy/cluster/views/tests/test_raster.py +++ b/phy/cluster/views/tests/test_raster.py @@ -1,24 +1,22 @@ -# -*- coding: utf-8 -*- - """Test scatter view.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import numpy as np - -from phylib.utils import connect from phylib.io.mock import artificial_spike_clusters, artificial_spike_samples +from phylib.utils import connect from phy.plot.tests import mouse_click + from ..raster import RasterView from . import _stop_and_close - -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test scatter view -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_raster_0(qtbot, gui): n = 5 @@ -26,8 +24,9 @@ def test_raster_0(qtbot, gui): spike_clusters = np.arange(n) cluster_ids = np.arange(n) - class Supervisor(object): + class Supervisor: pass + s = Supervisor() v = RasterView(spike_times, spike_clusters) @@ -58,16 +57,24 @@ def on_request_select(sender, cluster_ids): def on_select_more(sender, cluster_ids): _clicked.append(cluster_ids) - mouse_click(qtbot, v.canvas, pos=(w / 2, 0.), button='Left', modifiers=('Control',)) + mouse_click(qtbot, v.canvas, pos=(w / 2, 0.0), button='Left', modifiers=('Control',)) assert len(_clicked) == 1 assert _clicked == [[0]] mouse_click( - qtbot, v.canvas, pos=(w / 2, h / 2), button='Left', modifiers=('Control', 'Shift',)) + qtbot, + v.canvas, + pos=(w / 2, h / 2), + button='Left', + modifiers=( + 'Control', + 'Shift', + ), + ) assert len(_clicked) == 2 assert _clicked[1][0] in (1, 2) - v.zoom_to_time_range((1., 3.)) + v.zoom_to_time_range((1.0, 3.0)) _stop_and_close(qtbot, v) @@ -75,21 +82,25 @@ def on_select_more(sender, cluster_ids): def test_raster_1(qtbot, gui): ns = 10000 nc = 100 - spike_times = artificial_spike_samples(ns) / 20000. + spike_times = artificial_spike_samples(ns) / 20000.0 spike_clusters = artificial_spike_clusters(ns, nc) cluster_ids = np.arange(4) v = RasterView(spike_times, spike_clusters) @v.add_color_scheme( - name='group', cluster_ids=cluster_ids, - colormap='cluster_group', categorical=True) + name='group', cluster_ids=cluster_ids, colormap='cluster_group', categorical=True + ) def cg(cluster_id): return cluster_id % 4 v.add_color_scheme( - lambda cid: cid, name='random', cluster_ids=cluster_ids, - colormap='categorical', categorical=True) + lambda cid: cid, + name='random', + cluster_ids=cluster_ids, + colormap='categorical', + categorical=True, + ) v.show() qtbot.waitForWindowShown(v.canvas) diff --git a/phy/cluster/views/tests/test_scatter.py b/phy/cluster/views/tests/test_scatter.py index 139476060..83a75a097 100644 --- a/phy/cluster/views/tests/test_scatter.py +++ b/phy/cluster/views/tests/test_scatter.py @@ -1,28 +1,25 @@ -# -*- coding: utf-8 -*- - """Test scatter view.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import numpy as np +from phylib.utils import Bunch from pytest import raises -from phylib.utils import Bunch from phy.plot.tests import mouse_click + from ..scatter import ScatterView from . import _stop_and_close - -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test scatter view -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_scatter_view_0(qtbot, gui): - v = ScatterView( - coords=lambda cluster_ids, load_all=False: None - ) + v = ScatterView(coords=lambda cluster_ids, load_all=False: None) v.show() qtbot.waitForWindowShown(v.canvas) v.attach(gui) @@ -42,7 +39,8 @@ def test_scatter_view_1(qtbot, gui): x = np.zeros(1) v = ScatterView( coords=lambda cluster_ids: Bunch( - x=x, y=x, spike_ids=[0], spike_clusters=[0], data_bounds=(0, 0, 0, 0)) + x=x, y=x, spike_ids=[0], spike_clusters=[0], data_bounds=(0, 0, 0, 0) + ) ) v.show() qtbot.waitForWindowShown(v.canvas) @@ -54,12 +52,15 @@ def test_scatter_view_1(qtbot, gui): def test_scatter_view_2(qtbot, gui): n = 1000 v = ScatterView( - coords=lambda cluster_ids, load_all=False: [Bunch( - x=np.random.randn(n), - y=np.random.randn(n), - spike_ids=np.arange(n), - data_bounds=None, - ) for c in cluster_ids] + coords=lambda cluster_ids, load_all=False: [ + Bunch( + x=np.random.randn(n), + y=np.random.randn(n), + spike_ids=np.arange(n), + data_bounds=None, + ) + for c in cluster_ids + ] ) v.show() qtbot.waitForWindowShown(v.canvas) diff --git a/phy/cluster/views/tests/test_template.py b/phy/cluster/views/tests/test_template.py index a5c906f80..55bf27a7d 100644 --- a/phy/cluster/views/tests/test_template.py +++ b/phy/cluster/views/tests/test_template.py @@ -1,24 +1,22 @@ -# -*- coding: utf-8 -*- - """Test views.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import numpy as np - from phylib.io.mock import artificial_waveforms from phylib.utils import Bunch, connect from phy.plot.tests import mouse_click + from ..template import TemplateView from . import _stop_and_close - -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test template view -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_template_view_0(qtbot, tempdir, gui): n_samples = 50 @@ -26,10 +24,13 @@ def test_template_view_0(qtbot, tempdir, gui): channel_ids = np.arange(n_clusters + 2) def get_templates(cluster_ids): - return {i: Bunch( - template=artificial_waveforms(1, n_samples, 2)[0, ...], - channel_ids=np.arange(i, i + 2), - ) for i in cluster_ids} + return { + i: Bunch( + template=artificial_waveforms(1, n_samples, 2)[0, ...], + channel_ids=np.arange(i, i + 2), + ) + for i in cluster_ids + } v = TemplateView(templates=get_templates, channel_ids=channel_ids) v.show() @@ -48,13 +49,17 @@ def test_template_view_1(qtbot, tempdir, gui): cluster_ids = np.arange(n_clusters) def get_templates(cluster_ids): - return {i: Bunch( - template=artificial_waveforms(1, n_samples, 2)[0, ...], - channel_ids=np.arange(i, i + 2), - ) for i in cluster_ids} - - class Supervisor(object): + return { + i: Bunch( + template=artificial_waveforms(1, n_samples, 2)[0, ...], + channel_ids=np.arange(i, i + 2), + ) + for i in cluster_ids + } + + class Supervisor: pass + s = Supervisor() v = TemplateView(templates=get_templates, channel_ids=channel_ids, cluster_ids=cluster_ids) @@ -83,11 +88,20 @@ def on_request_select(sender, cluster_ids): def on_select_more(sender, cluster_ids): _clicked.append(cluster_ids) - mouse_click(qtbot, v.canvas, pos=(0, 0.), button='Left', modifiers=('Control',)) + mouse_click(qtbot, v.canvas, pos=(0, 0.0), button='Left', modifiers=('Control',)) assert len(_clicked) == 1 assert _clicked[0] in ([4], [5]) - mouse_click(qtbot, v.canvas, pos=(0, h / 2), button='Left', modifiers=('Control', 'Shift',)) + mouse_click( + qtbot, + v.canvas, + pos=(0, h / 2), + button='Left', + modifiers=( + 'Control', + 'Shift', + ), + ) assert len(_clicked) == 2 assert _clicked[1] == [9] diff --git a/phy/cluster/views/tests/test_trace.py b/phy/cluster/views/tests/test_trace.py index 85ed49b96..867492b87 100644 --- a/phy/cluster/views/tests/test_trace.py +++ b/phy/cluster/views/tests/test_trace.py @@ -1,34 +1,32 @@ -# -*- coding: utf-8 -*- - """Test views.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import numpy as np from numpy.testing import assert_allclose as ac - -from phylib.io.mock import artificial_traces, artificial_spike_clusters +from phylib.io.mock import artificial_spike_clusters, artificial_traces from phylib.utils import Bunch, connect from phylib.utils.geometry import linear_positions + from phy.plot.tests import mouse_click -from ..trace import TraceView, TraceImageView, select_traces, _iter_spike_waveforms +from ..trace import TraceImageView, TraceView, _iter_spike_waveforms, select_traces from . import _stop_and_close - -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test trace view -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_iter_spike_waveforms(): nc = 5 ns = 20 - sr = 2000. + sr = 2000.0 ch = list(range(nc)) - duration = 1. - st = np.linspace(0.1, .9, ns) + duration = 1.0 + st = np.linspace(0.1, 0.9, ns) sc = artificial_spike_clusters(ns, nc) traces = 10 * artificial_traces(int(round(duration * sr)), nc) @@ -36,13 +34,13 @@ def test_iter_spike_waveforms(): s = Bunch(cluster_meta={}, selected=[0]) for w in _iter_spike_waveforms( - interval=[0., 1.], - traces_interval=traces, - model=m, - supervisor=s, - n_samples_waveforms=ns, - show_all_spikes=True, - get_best_channels=lambda cluster_id: (ch, np.ones(nc)), + interval=[0.0, 1.0], + traces_interval=traces, + model=m, + supervisor=s, + n_samples_waveforms=ns, + show_all_spikes=True, + get_best_channels=lambda cluster_id: (ch, np.ones(nc)), ): assert w @@ -50,16 +48,16 @@ def test_iter_spike_waveforms(): def test_trace_view_1(qtbot, tempdir, gui): nc = 5 ns = 20 - sr = 2000. - duration = 1. - st = np.linspace(0.1, .9, ns) + sr = 2000.0 + duration = 1.0 + st = np.linspace(0.1, 0.9, ns) sc = artificial_spike_clusters(ns, nc) traces = 10 * artificial_traces(int(round(duration * sr)), nc) def get_traces(interval): out = Bunch( data=select_traces(traces, interval, sample_rate=sr), - color=(.75, .75, .75, 1), + color=(0.75, 0.75, 0.75, 1), ) a, b = st.searchsorted(interval) out.waveforms = [] @@ -69,7 +67,7 @@ def get_traces(interval): c = sc[i] s = int(round(t * sr)) d = Bunch( - data=traces[s - k:s + k, :], + data=traces[s - k : s + k, :], start_time=(s - k) / sr, channel_ids=np.arange(5), spike_id=i, @@ -101,25 +99,25 @@ def get_spike_times(): v.stacked.add_boxes(v.canvas) - ac(v.stacked.box_size, (.950, .165), atol=1e-3) - v.set_interval((.375, .625)) - assert v.time == .5 + ac(v.stacked.box_size, (0.950, 0.165), atol=1e-3) + v.set_interval((0.375, 0.625)) + assert v.time == 0.5 qtbot.wait(1) - v.go_to(.25) - assert v.time == .25 + v.go_to(0.25) + assert v.time == 0.25 qtbot.wait(1) - v.go_to(-.5) - assert v.time == .125 + v.go_to(-0.5) + assert v.time == 0.125 qtbot.wait(1) v.go_left() - assert v.time == .125 + assert v.time == 0.125 qtbot.wait(1) v.go_right() - ac(v.time, .150) + ac(v.time, 0.150) qtbot.wait(1) v.jump_left() @@ -135,16 +133,16 @@ def get_spike_times(): qtbot.wait(1) # Change interval size. - v.interval = (.25, .75) - ac(v.interval, (.25, .75)) + v.interval = (0.25, 0.75) + ac(v.interval, (0.25, 0.75)) qtbot.wait(1) v.widen() - ac(v.interval, (.1875, .8125)) + ac(v.interval, (0.1875, 0.8125)) qtbot.wait(1) v.narrow() - ac(v.interval, (.25, .75)) + ac(v.interval, (0.25, 0.75)) qtbot.wait(1) v.go_to_start() @@ -185,7 +183,7 @@ def get_spike_times(): qtbot.wait(1) v.increase() - ac(v.stacked.box_size, bs, atol=.05) + ac(v.stacked.box_size, bs, atol=0.05) qtbot.wait(1) v.origin = 'bottom' @@ -200,7 +198,7 @@ def get_spike_times(): def on_select_spike(sender, channel_id=None, spike_id=None, cluster_id=None, key=None): _clicked.append((channel_id, spike_id, cluster_id)) - mouse_click(qtbot, v.canvas, pos=(0., 0.), button='Left', modifiers=('Control',)) + mouse_click(qtbot, v.canvas, pos=(0.0, 0.0), button='Left', modifiers=('Control',)) v.set_state(v.state) @@ -213,28 +211,30 @@ def on_select_spike(sender, channel_id=None, spike_id=None, cluster_id=None, key def on_select_channel(sender, channel_id=None, button=None): _clicked.append((channel_id, button)) - mouse_click(qtbot, v.canvas, pos=(0., 0.), button='Left', modifiers=('Shift',)) - mouse_click(qtbot, v.canvas, pos=(0., 0.), button='Right', modifiers=('Shift',)) + mouse_click(qtbot, v.canvas, pos=(0.0, 0.0), button='Left', modifiers=('Shift',)) + mouse_click(qtbot, v.canvas, pos=(0.0, 0.0), button='Right', modifiers=('Shift',)) assert _clicked == [(2, 'Left'), (2, 'Right')] _stop_and_close(qtbot, v) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test trace imageview -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_trace_image_view_1(qtbot, tempdir, gui): nc = 350 - sr = 2000. - duration = 1. + sr = 2000.0 + duration = 1.0 traces = 10 * artificial_traces(int(round(duration * sr)), nc) def get_traces(interval): - return Bunch(data=select_traces(traces, interval, sample_rate=sr), - color=(.75, .75, .75, 1), - ) + return Bunch( + data=select_traces(traces, interval, sample_rate=sr), + color=(0.75, 0.75, 0.75, 1), + ) v = TraceImageView( traces=get_traces, @@ -249,24 +249,24 @@ def get_traces(interval): v.update_color() - v.set_interval((.375, .625)) - assert v.time == .5 + v.set_interval((0.375, 0.625)) + assert v.time == 0.5 qtbot.wait(1) - v.go_to(.25) - assert v.time == .25 + v.go_to(0.25) + assert v.time == 0.25 qtbot.wait(1) - v.go_to(-.5) - assert v.time == .125 + v.go_to(-0.5) + assert v.time == 0.125 qtbot.wait(1) v.go_left() - assert v.time == .125 + assert v.time == 0.125 qtbot.wait(1) v.go_right() - ac(v.time, .150) + ac(v.time, 0.150) qtbot.wait(1) v.jump_left() @@ -276,16 +276,16 @@ def get_traces(interval): qtbot.wait(1) # Change interval size. - v.interval = (.25, .75) - ac(v.interval, (.25, .75)) + v.interval = (0.25, 0.75) + ac(v.interval, (0.25, 0.75)) qtbot.wait(1) v.widen() - ac(v.interval, (.1875, .8125)) + ac(v.interval, (0.1875, 0.8125)) qtbot.wait(1) v.narrow() - ac(v.interval, (.25, .75)) + ac(v.interval, (0.25, 0.75)) qtbot.wait(1) v.go_to_start() diff --git a/phy/cluster/views/tests/test_waveform.py b/phy/cluster/views/tests/test_waveform.py index 3a50a9f88..1386f5969 100644 --- a/phy/cluster/views/tests/test_waveform.py +++ b/phy/cluster/views/tests/test_waveform.py @@ -1,26 +1,24 @@ -# -*- coding: utf-8 -*- - """Test views.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import numpy as np from numpy.testing import assert_allclose as ac - from phylib.io.mock import artificial_waveforms from phylib.utils import Bunch, connect from phylib.utils.geometry import staggered_positions -from phy.plot.tests import mouse_click, key_press, key_release + +from phy.plot.tests import key_press, key_release, mouse_click from ..waveform import WaveformView from . import _stop_and_close - -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test waveform view -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_waveform_view(qtbot, tempdir, gui): nc = 5 @@ -31,14 +29,15 @@ def test_waveform_view(qtbot, tempdir, gui): def get_waveforms(cluster_id): return Bunch( data=w, - masks=np.random.uniform(low=0., high=1., size=(ns, nc)), + masks=np.random.uniform(low=0.0, high=1.0, size=(ns, nc)), channel_ids=np.arange(nc), - channel_labels=['%d' % (ch * 10) for ch in range(nc)], - channel_positions=staggered_positions(nc)) + channel_labels=[f'{ch * 10}' for ch in range(nc)], + channel_positions=staggered_positions(nc), + ) v = WaveformView( waveforms={'waveforms': get_waveforms, 'mean_waveforms': get_waveforms}, - sample_rate=10000., + sample_rate=10000.0, ) v.show() qtbot.waitForWindowShown(v.canvas) @@ -99,7 +98,7 @@ def on_select_channel(sender, channel_id=None, button=None, key=None): _clicked.append((channel_id, button, key)) key_press(qtbot, v.canvas, '2') - mouse_click(qtbot, v.canvas, pos=(0., 0.), button='Left') + mouse_click(qtbot, v.canvas, pos=(0.0, 0.0), button='Left') key_release(qtbot, v.canvas, '2') assert _clicked == [(2, 'Left', 2)] diff --git a/phy/cluster/views/trace.py b/phy/cluster/views/trace.py index 07f4e6cc8..e204262e9 100644 --- a/phy/cluster/views/trace.py +++ b/phy/cluster/views/trace.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """Trace view.""" @@ -10,13 +8,19 @@ import logging import numpy as np - from phylib.utils import Bunch, emit -from phy.utils.color import selected_cluster_color, colormaps, _continuous_colormap, add_alpha + from phy.plot.interact import Stacked from phy.plot.transform import NDC, Range, _fix_coordinate_in_visual -from phy.plot.visuals import PlotVisual, UniformPlotVisual, TextVisual, ImageVisual -from .base import ManualClusteringView, ScalingMixin, BaseColorView +from phy.plot.visuals import ImageVisual, PlotVisual, TextVisual, UniformPlotVisual +from phy.utils.color import ( + _continuous_colormap, + add_alpha, + colormaps, + selected_cluster_color, +) + +from .base import BaseColorView, ManualClusteringView, ScalingMixin logger = logging.getLogger(__name__) @@ -25,6 +29,7 @@ # Trace view # ----------------------------------------------------------------------------- + def select_traces(traces, interval, sample_rate=None): """Load traces in an interval (in seconds).""" start, end = interval @@ -36,8 +41,14 @@ def select_traces(traces, interval, sample_rate=None): def _iter_spike_waveforms( - interval=None, traces_interval=None, model=None, supervisor=None, - n_samples_waveforms=None, get_best_channels=None, show_all_spikes=False): + interval=None, + traces_interval=None, + model=None, + supervisor=None, + n_samples_waveforms=None, + get_best_channels=None, + show_all_spikes=False, +): """Iterate through the spike waveforms belonging in the current trace view.""" m = model p = supervisor @@ -55,7 +66,7 @@ def _iter_spike_waveforms( if is_selected is not show_selected: continue # Skip non-selected spikes if requested. - if (not show_all_spikes and c not in supervisor.selected): + if not show_all_spikes and c not in supervisor.selected: continue # cg = p.cluster_meta.get('group', c) channel_ids, channel_amps = get_best_channels(c) @@ -65,7 +76,7 @@ def _iter_spike_waveforms( continue # Extract the waveform. wave = Bunch( - data=traces_interval[s - k:s + ns - k, channel_ids], + data=traces_interval[s - k : s + ns - k, channel_ids], channel_ids=channel_ids, start_time=(s + s0 - k) / sr, spike_id=i, @@ -106,16 +117,17 @@ class TraceView(ScalingMixin, BaseColorView, ManualClusteringView): Labels of all shown channels. By default, this is just the channel ids. """ + _default_position = 'left' auto_update = True auto_scale = True - interval_duration = .25 # default duration of the interval - shift_amount = .1 + interval_duration = 0.25 # default duration of the interval + shift_amount = 0.1 scaling_coeff_x = 1.25 - trace_quantile = .01 # quantile for auto-scaling - default_trace_color = (.5, .5, .5, 1) - trace_color_0 = (.353, .161, .443) - trace_color_1 = (.133, .404, .396) + trace_quantile = 0.01 # quantile for auto-scaling + default_trace_color = (0.5, 0.5, 0.5, 1) + trace_color_0 = (0.353, 0.161, 0.443) + trace_color_1 = (0.133, 0.404, 0.396) default_shortcuts = { 'change_trace_size': 'ctrl+wheel', 'switch_color_scheme': 'shift+wheel', @@ -146,9 +158,16 @@ class TraceView(ScalingMixin, BaseColorView, ManualClusteringView): } def __init__( - self, traces=None, sample_rate=None, spike_times=None, duration=None, - n_channels=None, channel_positions=None, channel_labels=None, **kwargs): - + self, + traces=None, + sample_rate=None, + spike_times=None, + duration=None, + n_channels=None, + channel_positions=None, + channel_labels=None, + **kwargs, + ): self.do_show_labels = True self.show_all_spikes = False @@ -157,10 +176,10 @@ def __init__( # Sample rate. assert sample_rate > 0 self.sample_rate = float(sample_rate) - self.dt = 1. / self.sample_rate + self.dt = 1.0 / self.sample_rate # Traces and spikes. - assert hasattr(traces, '__call__') + assert callable(traces) self.traces = traces # self.waveforms = None @@ -172,29 +191,41 @@ def __init__( # Channel y ranking. self.channel_positions = ( - channel_positions if channel_positions is not None else - np.c_[np.zeros(n_channels), np.arange(n_channels)]) + channel_positions + if channel_positions is not None + else np.c_[np.zeros(n_channels), np.arange(n_channels)] + ) # channel_y_ranks[i] is the position of channel #i in the trace view. self.channel_y_ranks = np.argsort(np.argsort(self.channel_positions[:, 1])) assert self.channel_y_ranks.shape == (n_channels,) # Channel labels. self.channel_labels = ( - channel_labels if channel_labels is not None else - ['%d' % ch for ch in range(n_channels)]) - assert len(self.channel_labels) == n_channels + channel_labels + if channel_labels is not None + else [f'{ch}' for ch in range(n_channels)] + ) + assert len(self.channel_labels) == self.n_channels # Initialize the view. - super(TraceView, self).__init__(**kwargs) - self.state_attrs += ('origin', 'do_show_labels', 'show_all_spikes', 'auto_scale') - self.local_state_attrs += ('interval', 'scaling',) + super().__init__(**kwargs) + self.state_attrs += ( + 'origin', + 'do_show_labels', + 'show_all_spikes', + 'auto_scale', + ) + self.local_state_attrs += ( + 'interval', + 'scaling', + ) # Visuals. self._create_visuals() # Initial interval. self._interval = None - self.go_to(duration / 2.) + self.go_to(duration / 2.0) self._waveform_times = [] self.canvas.panzoom.set_constrain_bounds((-1, -2, +1, +2)) @@ -207,8 +238,10 @@ def _create_visuals(self): # Gradient of color for the traces. if self.trace_color_0 and self.trace_color_1: self.trace_visual.inserter.insert_frag( - 'gl_FragColor.rgb = mix(vec3%s, vec3%s, (v_signal_index / %d));' % ( - self.trace_color_0, self.trace_color_1, self.n_channels), 'end') + f'gl_FragColor.rgb = mix(vec3{self.trace_color_0}, ' + f'vec3{self.trace_color_1}, (v_signal_index / {self.n_channels}));', + 'end', + ) self.canvas.add_visual(self.trace_visual) self.waveform_visual = PlotVisual() @@ -217,9 +250,11 @@ def _create_visuals(self): self.text_visual = TextVisual() _fix_coordinate_in_visual(self.text_visual, 'x') self.text_visual.inserter.add_varying( - 'float', 'v_discard', + 'float', + 'v_discard', 'float((n_boxes >= 50 * u_zoom.y) && ' - '(mod(int(a_box_index), int(n_boxes / (50 * u_zoom.y))) >= 1))') + '(mod(int(a_box_index), int(n_boxes / (50 * u_zoom.y))) >= 1))', + ) self.text_visual.inserter.insert_frag('if (v_discard > 0) discard;', 'end') self.canvas.add_visual(self.text_visual) @@ -250,7 +285,8 @@ def _plot_traces(self, traces, color=None): self.trace_visual.color = color self.canvas.update_visual( self.trace_visual, - t, traces, + t, + traces, data_bounds=self.data_bounds, box_index=box_index.ravel(), ) @@ -268,7 +304,9 @@ def _plot_spike(self, bunch): i = bunch.select_index c = bunch.spike_cluster cs = self.color_schemes.get() - color = selected_cluster_color(i, alpha=1) if i is not None else cs.get(c, alpha=1) + color = ( + selected_cluster_color(i, alpha=1) if i is not None else cs.get(c, alpha=1) + ) # We could tweak the color of each spike waveform depending on the template amplitude # on each of its best channels. @@ -283,7 +321,9 @@ def _plot_spike(self, bunch): box_index = np.repeat(box_index[:, np.newaxis], n_samples, axis=0) self.waveform_visual.add_batch_data( box_index=box_index, - x=t, y=bunch.data.T, color=color, + x=t, + y=bunch.data.T, + color=color, data_bounds=self.data_bounds, ) @@ -297,7 +337,13 @@ def _plot_waveforms(self, waveforms, **kwargs): for w in waveforms: self._plot_spike(w) self._waveform_times.append( - (w.start_time, w.spike_id, w.spike_cluster, w.get('channel_ids', None))) + ( + w.start_time, + w.spike_id, + w.spike_cluster, + w.get('channel_ids', None), + ) + ) self.canvas.update_visual(self.waveform_visual) else: # pragma: no cover self.waveform_visual.hide() @@ -310,7 +356,7 @@ def _plot_labels(self, traces): self.text_visual.add_batch_data( pos=[self.data_bounds[0], 0], text=ch_label, - anchor=[+1., 0], + anchor=[+1.0, 0], data_bounds=self.data_bounds, box_index=bi, ) @@ -327,10 +373,10 @@ def _restrict_interval(self, interval): end = int(round(end * self.sample_rate)) / self.sample_rate # Restrict the interval to the boundaries of the traces. if start < 0: - end += (-start) + end += -start start = 0 elif end >= self.duration: - start -= (end - self.duration) + start -= end - self.duration end = self.duration start = np.clip(start, 0, end) end = np.clip(end, start, self.duration) @@ -343,13 +389,13 @@ def plot(self, update_traces=True, update_waveforms=True): traces = self.traces(self._interval) if update_traces: - logger.log(5, "Redraw the entire trace view.") + logger.log(5, 'Redraw the entire trace view.') start, end = self._interval # Find the data bounds. if self.auto_scale or getattr(self, 'data_bounds', NDC) == NDC: ymin = np.quantile(traces.data, self.trace_quantile) - ymax = np.quantile(traces.data, 1. - self.trace_quantile) + ymax = np.quantile(traces.data, 1.0 - self.trace_quantile) else: ymin, ymax = self.data_bounds[1], self.data_bounds[3] self.data_bounds = (start, ymin, end, ymax) @@ -358,8 +404,7 @@ def plot(self, update_traces=True, update_waveforms=True): self._waveform_times = [] # Plot the traces. - self._plot_traces( - traces.data, color=traces.get('color', None)) + self._plot_traces(traces.data, color=traces.get('color', None)) # Plot the labels. if self.do_show_labels: @@ -378,7 +423,7 @@ def set_interval(self, interval=None): interval = self._restrict_interval(interval) if interval != self._interval: - logger.log(5, "Redraw the entire trace view.") + logger.log(5, 'Redraw the entire trace view.') self._interval = interval emit('is_busy', self, True) self.plot(update_traces=True, update_waveforms=True) @@ -397,17 +442,21 @@ def on_select(self, cluster_ids=None, **kwargs): def attach(self, gui): """Attach the view to the GUI.""" - super(TraceView, self).attach(gui) + super().attach(gui) - self.actions.add(self.toggle_show_labels, checkable=True, checked=self.do_show_labels) self.actions.add( - self.toggle_highlighted_spikes, checkable=True, checked=self.show_all_spikes) - self.actions.add(self.toggle_auto_scale, checkable=True, checked=self.auto_scale) + self.toggle_show_labels, checkable=True, checked=self.do_show_labels + ) + self.actions.add( + self.toggle_highlighted_spikes, checkable=True, checked=self.show_all_spikes + ) + self.actions.add( + self.toggle_auto_scale, checkable=True, checked=self.auto_scale + ) self.actions.add(self.switch_origin) self.actions.separator() - self.actions.add( - self.go_to, prompt=True, prompt_default=lambda: str(self.time)) + self.actions.add(self.go_to, prompt=True, prompt_default=lambda: str(self.time)) self.actions.separator() self.actions.add(self.go_to_start) @@ -434,7 +483,7 @@ def attach(self, gui): @property def status(self): a, b = self._interval - return '[{:.2f}s - {:.2f}s]. Color scheme: {}.'.format(a, b, self.color_scheme) + return f'[{a:.2f}s - {b:.2f}s]. Color scheme: {self.color_scheme}.' # Origin # ------------------------------------------------------------------------- @@ -453,8 +502,9 @@ def origin(self, value): self.canvas.layout.origin = value else: # pragma: no cover logger.warning( - "Could not set origin to %s because the layout instance was not initialized yet.", - value) + 'Could not set origin to %s because the layout instance was not initialized yet.', + value, + ) def switch_origin(self): """Switch between top and bottom origin for the channels.""" @@ -466,7 +516,7 @@ def switch_origin(self): @property def time(self): """Time at the center of the window.""" - return sum(self._interval) * .5 + return sum(self._interval) * 0.5 @property def interval(self): @@ -482,9 +532,9 @@ def half_duration(self): """Half of the duration of the current interval.""" if self._interval is not None: a, b = self._interval - return (b - a) * .5 + return (b - a) * 0.5 else: - return self.interval_duration * .5 + return self.interval_duration * 0.5 def go_to(self, time): """Go to a specific time (in seconds).""" @@ -506,23 +556,23 @@ def go_to_end(self): def go_right(self): """Go to right.""" start, end = self._interval - delay = (end - start) * .1 + delay = (end - start) * 0.1 self.shift(delay) def go_left(self): """Go to left.""" start, end = self._interval - delay = (end - start) * .1 + delay = (end - start) * 0.1 self.shift(-delay) def jump_right(self): """Jump to right.""" - delay = self.duration * .1 + delay = self.duration * 0.1 self.shift(delay) def jump_left(self): """Jump to left.""" - delay = self.duration * .1 + delay = self.duration * 0.1 self.shift(-delay) def _jump_to_spike(self, delta=+1): @@ -533,11 +583,15 @@ def _jump_to_spike(self, delta=+1): n = len(spike_times) self.go_to(spike_times[(ind + delta) % n]) - def go_to_next_spike(self, ): + def go_to_next_spike( + self, + ): """Jump to the next spike from the first selected cluster.""" self._jump_to_spike(+1) - def go_to_previous_spike(self, ): + def go_to_previous_spike( + self, + ): """Jump to the previous spike from the first selected cluster.""" self._jump_to_spike(-1) @@ -563,14 +617,14 @@ def narrow(self): def toggle_show_labels(self, checked): """Toggle the display of the channel ids.""" - logger.debug("Set show labels to %s.", checked) + logger.debug('Set show labels to %s.', checked) self.do_show_labels = checked self.text_visual.toggle() self.canvas.update() def toggle_auto_scale(self, checked): """Toggle automatic scaling of the traces.""" - logger.debug("Set auto scale to %s.", checked) + logger.debug('Set auto scale to %s.', checked) self.auto_scale = checked def update_color(self): @@ -608,7 +662,11 @@ def on_mouse_click(self, e): # Find the spike and cluster closest to the mouse. db = self.data_bounds # Get the information about the displayed spikes. - wt = [(t, s, c, ch) for t, s, c, ch in self._waveform_times if channel_id in ch] + wt = [ + (t, s, c, ch) + for t, s, c, ch in self._waveform_times + if channel_id in ch + ] if not wt: return # Get the time coordinate of the mouse position. @@ -620,8 +678,13 @@ def on_mouse_click(self, e): # Raise the select_spike event. spike_id = spike_ids[i] cluster_id = spike_clusters[i] - emit('select_spike', self, channel_id=channel_id, - spike_id=spike_id, cluster_id=cluster_id) + emit( + 'select_spike', + self, + channel_id=channel_id, + spike_id=spike_id, + cluster_id=cluster_id, + ) if 'Shift' in e.modifiers: # Get mouse position in NDC. @@ -631,10 +694,10 @@ def on_mouse_click(self, e): def on_mouse_wheel(self, e): # pragma: no cover """Scroll through the data with alt+wheel.""" - super(TraceView, self).on_mouse_wheel(e) + super().on_mouse_wheel(e) if e.modifiers == ('Alt',): start, end = self._interval - delay = e.delta * (end - start) * .1 + delay = e.delta * (end - start) * 0.1 self.shift(-delay) @@ -642,6 +705,7 @@ def on_mouse_wheel(self, e): # pragma: no cover # Trace Image view # ----------------------------------------------------------------------------- + class TraceImageView(TraceView): """This view shows the raw traces as an image @@ -666,6 +730,7 @@ class TraceImageView(TraceView): Labels of all shown channels. By default, this is just the channel ids. """ + default_shortcuts = { 'change_trace_size': 'ctrl+wheel', 'decrease': 'ctrl+alt+down', @@ -691,10 +756,13 @@ def __init__(self, **kwargs): self._scaling = 1 self.vrange = (0, 1) - super(TraceImageView, self).__init__(**kwargs) + super().__init__(**kwargs) self.state_attrs += ('origin', 'auto_scale') - self.local_state_attrs += ('interval', 'scaling',) + self.local_state_attrs += ( + 'interval', + 'scaling', + ) def _create_visuals(self): self.trace_visual = ImageVisual() @@ -714,7 +782,7 @@ def _plot_traces(self, traces, color=None): vmin, vmax = self.vrange image = _continuous_colormap(colormaps.diverging, traces, vmin=vmin, vmax=vmax) - image = add_alpha(image, alpha=1.) + image = add_alpha(image, alpha=1.0) self.trace_visual.set_data(image=image) # Public methods @@ -722,21 +790,20 @@ def _plot_traces(self, traces, color=None): def plot(self, update_traces=True, **kwargs): if update_traces: - logger.log(5, "Redraw the entire trace view.") + logger.log(5, 'Redraw the entire trace view.') traces = self.traces(self._interval) # Find the data bounds. if self.auto_scale or self.vrange == (0, 1): vmin = np.quantile(traces.data, self.trace_quantile) - vmax = np.quantile(traces.data, 1. - self.trace_quantile) + vmax = np.quantile(traces.data, 1.0 - self.trace_quantile) else: # pragma: no cover vmin, vmax = self.vrange self.vrange = (vmin * self.scaling, vmax * self.scaling) # Plot the traces. - self._plot_traces( - traces.data, color=traces.get('color', None)) + self._plot_traces(traces.data, color=traces.get('color', None)) self.canvas.update() diff --git a/phy/cluster/views/waveform.py b/phy/cluster/views/waveform.py index 237a42053..c244d0bd6 100644 --- a/phy/cluster/views/waveform.py +++ b/phy/cluster/views/waveform.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """Waveform view.""" @@ -7,18 +5,26 @@ # Imports # ----------------------------------------------------------------------------- -from collections import defaultdict import logging +from collections import defaultdict import numpy as np - from phylib.io.array import _flatten, _index_of from phylib.utils import emit -from phy.utils.color import selected_cluster_color + +from phy.cluster._utils import RotatingProperty from phy.plot import get_linear_x from phy.plot.visuals import ( # noqa - PlotVisual, PlotAggVisual, UniformScatterVisual, TextVisual, LineVisual, _min, _max) -from phy.cluster._utils import RotatingProperty + LineVisual, + PlotAggVisual, + PlotVisual, + TextVisual, + UniformScatterVisual, + _max, + _min, +) +from phy.utils.color import selected_cluster_color + from .base import ManualClusteringView, ScalingMixin logger = logging.getLogger(__name__) @@ -28,6 +34,7 @@ # Waveform view # ----------------------------------------------------------------------------- + def _get_box_pos(bunchs, channel_ids): cp = {} for d in bunchs: @@ -89,8 +96,8 @@ class WaveformView(ScalingMixin, ManualClusteringView): max_n_clusters = 8 _default_position = 'right' - ax_color = (.75, .75, .75, 1.) - tick_size = 5. + ax_color = (0.75, 0.75, 0.75, 1.0) + tick_size = 5.0 cluster_ids = () default_shortcuts = { @@ -99,14 +106,12 @@ class WaveformView(ScalingMixin, ManualClusteringView): 'next_waveforms_type': 'w', 'previous_waveforms_type': 'shift+w', 'toggle_mean_waveforms': 'm', - # Box scaling. 'widen': 'ctrl+right', 'narrow': 'ctrl+left', 'increase': 'ctrl+up', 'decrease': 'ctrl+down', 'change_box_size': 'ctrl+wheel', - # Probe scaling. 'extend_horizontally': 'shift+right', 'shrink_horizontally': 'shift+left', @@ -122,14 +127,16 @@ def __init__(self, waveforms=None, waveforms_type=None, sample_rate=None, **kwar self.do_show_labels = True self.channel_ids = None self.filtered_tags = () - self.wave_duration = 0. # updated in the plotting method + self.wave_duration = 0.0 # updated in the plotting method self.data_bounds = None self.sample_rate = sample_rate self._status_suffix = '' - assert sample_rate > 0., "The sample rate must be provided to the waveform view." + assert sample_rate > 0.0, ( + 'The sample rate must be provided to the waveform view.' + ) # Initialize the view. - super(WaveformView, self).__init__(**kwargs) + super().__init__(**kwargs) self.state_attrs += ('waveforms_type', 'overlap', 'do_show_labels') self.local_state_attrs += ('box_scaling', 'probe_scaling') @@ -138,7 +145,9 @@ def __init__(self, waveforms=None, waveforms_type=None, sample_rate=None, **kwar # Ensure waveforms is a dictionary, even if there is a single waveforms type. waveforms = waveforms or {} - waveforms = waveforms if isinstance(waveforms, dict) else {'waveforms': waveforms} + waveforms = ( + waveforms if isinstance(waveforms, dict) else {'waveforms': waveforms} + ) self.waveforms = waveforms # Rotating property waveforms types. @@ -156,7 +165,8 @@ def __init__(self, waveforms=None, waveforms_type=None, sample_rate=None, **kwar self.canvas.add_visual(self.line_visual) self.tick_visual = UniformScatterVisual( - marker='vbar', color=self.ax_color, size=self.tick_size) + marker='vbar', color=self.ax_color, size=self.tick_size + ) self.canvas.add_visual(self.tick_visual) # Two types of visuals: thin raw line visual for normal waveforms, thick antialiased @@ -187,7 +197,8 @@ def get_clusters_data(self): if self.waveforms_type not in self.waveforms: return bunchs = [ - self.waveforms_types.get()(cluster_id) for cluster_id in self.cluster_ids] + self.waveforms_types.get()(cluster_id) for cluster_id in self.cluster_ids + ] clu_offsets = _get_clu_offsets(bunchs) n_clu = max(clu_offsets) + 1 # Offset depending on the overlap. @@ -195,7 +206,7 @@ def get_clusters_data(self): bunch.index = i bunch.offset = offset bunch.n_clu = n_clu - bunch.color = selected_cluster_color(i, bunch.get('alpha', .75)) + bunch.color = selected_cluster_color(i, bunch.get('alpha', 0.75)) return bunchs def _plot_cluster(self, bunch): @@ -216,12 +227,14 @@ def _plot_cluster(self, bunch): # Find the x coordinates. t = get_linear_x(n_spikes_clu * n_channels, n_samples) - t = _overlap_transform(t, offset=bunch.offset, n=bunch.n_clu, overlap=self.overlap) + t = _overlap_transform( + t, offset=bunch.offset, n=bunch.n_clu, overlap=self.overlap + ) # HACK: on the GPU, we get the actual masks with fract(masks) # since we add the relative cluster index. We need to ensure # that the masks is never 1.0, otherwise it is interpreted as # 0. - eps = .001 + eps = 0.001 masks = eps + (1 - 2 * eps) * masks # NOTE: we add the cluster index which is used for the # computation of the depth on the GPU. @@ -248,8 +261,13 @@ def _plot_cluster(self, bunch): assert self.data_bounds is not None self._current_visual.add_batch_data( - x=t, y=wave, color=bunch.color, masks=masks, box_index=box_index, - data_bounds=self.data_bounds) + x=t, + y=wave, + color=bunch.color, + masks=masks, + box_index=box_index, + data_bounds=self.data_bounds, + ) # Waveform axes. # -------------- @@ -257,7 +275,8 @@ def _plot_cluster(self, bunch): # Horizontal y=0 lines. ax_db = self.data_bounds a, b = _overlap_transform( - np.array([-1, 1]), offset=bunch.offset, n=bunch.n_clu, overlap=self.overlap) + np.array([-1, 1]), offset=bunch.offset, n=bunch.n_clu, overlap=self.overlap + ) box_index = _index_of(channel_ids_loc, self.channel_ids) box_index = np.repeat(box_index, 2) box_index = np.tile(box_index, n_spikes_clu) @@ -273,18 +292,21 @@ def _plot_cluster(self, bunch): # Vertical ticks every millisecond. steps = np.arange(np.round(self.wave_duration * 1000)) # A vline every millisecond. - x = .001 * steps + x = 0.001 * steps # Scale to [-1, 1], same coordinates as the waveform points. x = -1 + 2 * x / self.wave_duration # Take overlap into account. - x = _overlap_transform(x, offset=bunch.offset, n=bunch.n_clu, overlap=self.overlap) + x = _overlap_transform( + x, offset=bunch.offset, n=bunch.n_clu, overlap=self.overlap + ) x = np.tile(x, len(channel_ids_loc)) # Generate the box index. box_index = _index_of(channel_ids_loc, self.channel_ids) box_index = np.repeat(box_index, x.size // len(box_index)) assert x.size == box_index.size self.tick_visual.add_batch_data( - x=x, y=np.zeros_like(x), + x=x, + y=np.zeros_like(x), data_bounds=ax_db, box_index=box_index, ) @@ -318,14 +340,15 @@ def plot(self, **kwargs): if bunchs[0].data is not None: self.wave_duration = bunchs[0].data.shape[1] / float(self.sample_rate) else: # pragma: no cover - self.wave_duration = 1. + self.wave_duration = 1.0 # Channel labels. channel_labels = {} for d in bunchs: - chl = d.get('channel_labels', ['%d' % ch for ch in d.channel_ids]) - channel_labels.update({ - channel_id: chl[i] for i, channel_id in enumerate(d.channel_ids)}) + chl = d.get('channel_labels', [f'{ch}' for ch in d.channel_ids]) + channel_labels.update( + {channel_id: chl[i] for i, channel_id in enumerate(d.channel_ids)} + ) # Update the Boxed box positions as a function of the selected channels. if channel_ids: @@ -357,10 +380,14 @@ def plot(self, **kwargs): def attach(self, gui): """Attach the view to the GUI.""" - super(WaveformView, self).attach(gui) + super().attach(gui) - self.actions.add(self.toggle_waveform_overlap, checkable=True, checked=self.overlap) - self.actions.add(self.toggle_show_labels, checkable=True, checked=self.do_show_labels) + self.actions.add( + self.toggle_waveform_overlap, checkable=True, checked=self.overlap + ) + self.actions.add( + self.toggle_show_labels, checkable=True, checked=self.do_show_labels + ) self.actions.add(self.next_waveforms_type) self.actions.add(self.previous_waveforms_type) self.actions.add(self.toggle_mean_waveforms, checkable=True) @@ -472,13 +499,15 @@ def toggle_show_labels(self, checked): def on_mouse_click(self, e): """Select a channel by clicking on a box in the waveform view.""" b = e.button - nums = tuple('%d' % i for i in range(10)) + nums = tuple(f'{i}' for i in range(10)) if 'Control' in e.modifiers or e.key in nums: key = int(e.key) if e.key in nums else None # Get mouse position in NDC. channel_idx, _ = self.canvas.boxed.box_map(e.pos) channel_id = self.channel_ids[channel_idx] - logger.debug("Click on channel_id %d with key %s and button %s.", channel_id, key, b) + logger.debug( + 'Click on channel_id %d with key %s and button %s.', channel_id, key, b + ) emit('select_channel', self, channel_id=channel_id, key=key, button=b) @property @@ -492,22 +521,22 @@ def waveforms_type(self, value): def next_waveforms_type(self): """Switch to the next waveforms type.""" self.waveforms_types.next() - logger.debug("Switch to waveforms type %s.", self.waveforms_type) + logger.debug('Switch to waveforms type %s.', self.waveforms_type) self.plot() def previous_waveforms_type(self): """Switch to the previous waveforms type.""" self.waveforms_types.previous() - logger.debug("Switch to waveforms type %s.", self.waveforms_type) + logger.debug('Switch to waveforms type %s.', self.waveforms_type) self.plot() def toggle_mean_waveforms(self, checked): """Switch to the `mean_waveforms` type, if it is available.""" if self.waveforms_type == 'mean_waveforms' and 'waveforms' in self.waveforms: self.waveforms_types.set('waveforms') - logger.debug("Switch to raw waveforms.") + logger.debug('Switch to raw waveforms.') self.plot() elif 'mean_waveforms' in self.waveforms: self.waveforms_types.set('mean_waveforms') - logger.debug("Switch to mean waveforms.") + logger.debug('Switch to mean waveforms.') self.plot() diff --git a/phy/conftest.py b/phy/conftest.py index 5f8eacff5..6772dcbe6 100644 --- a/phy/conftest.py +++ b/phy/conftest.py @@ -1,24 +1,20 @@ -# -*- coding: utf-8 -*- - """py.test utilities.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import logging -import numpy as np import warnings import matplotlib - +import numpy as np from phylib import add_default_handler from phylib.conftest import * # noqa - -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Common fixtures -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ logger = logging.getLogger('phy') logger.setLevel(10) diff --git a/phy/gui/__init__.py b/phy/gui/__init__.py index 456fd0514..75d62bb6c 100644 --- a/phy/gui/__init__.py +++ b/phy/gui/__init__.py @@ -1,11 +1,21 @@ -# -*- coding: utf-8 -*- # flake8: noqa """GUI routines.""" from .qt import ( - require_qt, create_app, run_app, prompt, message_box, input_dialog, busy_cursor, - screenshot, screen_size, is_high_dpi, thread_pool, Worker, Debouncer + require_qt, + create_app, + run_app, + prompt, + message_box, + input_dialog, + busy_cursor, + screenshot, + screen_size, + is_high_dpi, + thread_pool, + Worker, + Debouncer, ) from .gui import GUI, GUIState, DockWidget from .actions import Actions, Snippets diff --git a/phy/gui/actions.py b/phy/gui/actions.py index 4364ccde2..90efc805f 100644 --- a/phy/gui/actions.py +++ b/phy/gui/actions.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """Actions and snippets.""" @@ -8,15 +6,16 @@ # ----------------------------------------------------------------------------- import inspect -from functools import partial, wraps import logging import re import sys import traceback +from functools import partial, wraps -from .qt import QKeySequence, QAction, require_qt, input_dialog, busy_cursor, _get_icon from phylib.utils import Bunch +from .qt import QAction, QKeySequence, _get_icon, busy_cursor, input_dialog, require_qt + logger = logging.getLogger(__name__) @@ -24,6 +23,7 @@ # Snippet parsing utilities # ----------------------------------------------------------------------------- + def _parse_arg(s): """Parse a number or string.""" try: @@ -64,13 +64,13 @@ def _prompt_args(title, docstring, default=None): # There are args, need to display the dialog. # Extract Example: `...` in the docstring to put a predefined text # in the input dialog. - logger.debug("Prompting arguments for %s", title) + logger.debug('Prompting arguments for %s', title) r = re.search('Example: `([^`]+)`', docstring) - docstring_ = docstring[:r.start()].strip() if r else docstring + docstring_ = docstring[: r.start()].strip() if r else docstring try: text = str(default()) if default else (r.group(1) if r else None) except Exception as e: # pragma: no cover - logger.error("Error while handling user input: %s", str(e)) + logger.error('Error while handling user input: %s', str(e)) return s, ok = input_dialog(title, docstring_, text) if not ok or not s: @@ -84,6 +84,7 @@ def _prompt_args(title, docstring, default=None): # Show shortcut utility functions # ----------------------------------------------------------------------------- + def _get_shortcut_string(shortcut): """Return a string representation of a shortcut.""" if not shortcut: @@ -120,7 +121,7 @@ def _show_shortcuts(shortcuts): for n in sorted(shortcuts): shortcut = _get_shortcut_string(shortcuts[n]) if not n.startswith('_') and not shortcut.startswith('-'): - out.append('- {0:<40} {1:s}'.format(n, shortcut)) + out.append(f'- {n:<40} {shortcut:s}') if out: print('Keyboard shortcuts') print('\n'.join(out)) @@ -133,7 +134,7 @@ def _show_snippets(snippets): for n in sorted(snippets): snippet = snippets[n] if not n.startswith('_'): - out.append('- {0:<40} :{1:s}'.format(n, snippet)) + out.append(f'- {n:<40} :{snippet:s}') if out: print('Snippets') print('\n'.join(out)) @@ -153,6 +154,7 @@ def show_shortcuts_snippets(actions): # Actions # ----------------------------------------------------------------------------- + def _alias(name): # Get the alias from the character after & if it exists. alias = name[name.index('&') + 1] if '&' in name else name @@ -170,10 +172,10 @@ def _expected_args(f): f_args.remove('self') # Remove arguments with defaults from the list. if len(argspec.defaults or ()): - f_args = f_args[:-len(argspec.defaults)] + f_args = f_args[: -len(argspec.defaults)] # Remove arguments supplied in a partial. if isinstance(f, partial): - f_args = f_args[len(f.args):] + f_args = f_args[len(f.args) :] f_args = [arg for arg in f_args if arg not in f.keywords] return tuple(f_args) @@ -186,51 +188,51 @@ def _create_qaction(gui, **kwargs): action = QAction(name, gui) # Show an input dialog if there are args. - callback = kwargs.get('callback', None) + callback = kwargs.get('callback') title = getattr(callback, '__name__', 'action') # Number of expected arguments. - n_args = kwargs.get('n_args', None) or len(_expected_args(callback)) + n_args = kwargs.get('n_args') or len(_expected_args(callback)) @wraps(callback) def wrapped(is_checked, *args): - if kwargs.get('checkable', None): + if kwargs.get('checkable'): args = (is_checked,) + args - if kwargs.get('prompt', None): - args += _prompt_args( - title, docstring, default=kwargs.get('prompt_default', None)) or () + if kwargs.get('prompt'): + args += _prompt_args(title, docstring, default=kwargs.get('prompt_default')) or () if not args: # pragma: no cover - logger.debug("User cancelled input prompt, aborting.") + logger.debug('User cancelled input prompt, aborting.') return if len(args) < n_args: logger.warning( - "Invalid function arguments: expecting %d but got %d", n_args, len(args)) + 'Invalid function arguments: expecting %d but got %d', n_args, len(args) + ) return try: # Set a busy cursor if set_busy is True. - with busy_cursor(kwargs.get('set_busy', None)): + with busy_cursor(kwargs.get('set_busy')): return callback(*args) except Exception: # pragma: no cover - logger.warning("Error when executing action %s.", name) + logger.warning('Error when executing action %s.', name) logger.debug(''.join(traceback.format_exception(*sys.exc_info()))) action.triggered.connect(wrapped) - sequence = _get_qkeysequence(kwargs.get('shortcut', None)) + sequence = _get_qkeysequence(kwargs.get('shortcut')) if not isinstance(sequence, (tuple, list)): sequence = [sequence] action.setShortcuts(sequence) - assert kwargs.get('docstring', None) - docstring = re.sub(r'\s+', ' ', kwargs.get('docstring', None)) - docstring += ' (alias: {})'.format(kwargs.get('alias', None)) + assert kwargs.get('docstring') + docstring = re.sub(r'\s+', ' ', kwargs.get('docstring')) + docstring += f' (alias: {kwargs.get("alias")})' action.setStatusTip(docstring) action.setWhatsThis(docstring) - action.setCheckable(kwargs.get('checkable', None)) - action.setChecked(kwargs.get('checked', None)) - if kwargs.get('icon', None): + action.setCheckable(kwargs.get('checkable')) + action.setChecked(kwargs.get('checked')) + if kwargs.get('icon'): action.setIcon(_get_icon(kwargs['icon'])) return action -class Actions(object): +class Actions: """Group of actions bound to a GUI. This class attaches to a GUI and implements the following features: @@ -255,9 +257,18 @@ class Actions(object): Map action names to snippets (regular strings). """ + def __init__( - self, gui, name=None, menu=None, submenu=None, view=None, - insert_menu_before=None, default_shortcuts=None, default_snippets=None): + self, + gui, + name=None, + menu=None, + submenu=None, + view=None, + insert_menu_before=None, + default_shortcuts=None, + default_snippets=None, + ): self._actions_dict = {} self._aliases = {} self._default_shortcuts = default_shortcuts or {} @@ -302,10 +313,28 @@ def _get_menu(self, menu=None, submenu=None, view=None, view_submenu=None): if menu: return self.gui.get_menu(menu) - def add(self, callback=None, name=None, shortcut=None, alias=None, prompt=False, n_args=None, - docstring=None, menu=None, submenu=None, view=None, view_submenu=None, verbose=True, - checkable=False, checked=False, set_busy=False, prompt_default=None, - show_shortcut=True, icon=None, toolbar=False): + def add( + self, + callback=None, + name=None, + shortcut=None, + alias=None, + prompt=False, + n_args=None, + docstring=None, + menu=None, + submenu=None, + view=None, + view_submenu=None, + verbose=True, + checkable=False, + checked=False, + set_busy=False, + prompt_default=None, + show_shortcut=True, + icon=None, + toolbar=False, + ): """Add an action with a keyboard shortcut. Parameters @@ -381,14 +410,15 @@ def add(self, callback=None, name=None, shortcut=None, alias=None, prompt=False, action = _create_qaction(self.gui, **kwargs) action_obj = Bunch(qaction=action, **kwargs) if verbose and not name.startswith('_'): - logger.log(5, "Add action `%s` (%s).", name, _get_shortcut_string(action.shortcut())) + logger.log(5, 'Add action `%s` (%s).', name, _get_shortcut_string(action.shortcut())) self.gui.addAction(action) # Do not show private actions in the menu. if not name.startswith('_'): # Find the menu in which the action should be added. qmenu = self._get_menu( - menu=menu, submenu=submenu, view=view, view_submenu=view_submenu) + menu=menu, submenu=submenu, view=view, view_submenu=view_submenu + ) if qmenu: qmenu.addAction(action) @@ -453,13 +483,13 @@ def run(self, name, *args): # Get the action. action = self._actions_dict.get(name, None) if not action: - raise ValueError("Action `{}` doesn't exist.".format(name)) + raise ValueError(f"Action `{name}` doesn't exist.") if not name.startswith('_'): - logger.debug("Execute action `%s`.", name) + logger.debug('Execute action `%s`.', name) try: return action.callback(*args) except TypeError as e: - logger.warning("Invalid action arguments: " + str(e)) + logger.warning(f'Invalid action arguments: {str(e)}') return def remove(self, name): @@ -486,10 +516,10 @@ def shortcuts(self): if not action.shortcut and not action.alias: continue # Only show alias for actions with no shortcut. - alias_str = ' (:%s)' % action.alias if action.alias != name else '' + alias_str = f' (:{action.alias})' if action.alias != name else '' shortcut = action.shortcut or '-' shortcut = shortcut if isinstance(action.shortcut, str) else ', '.join(shortcut) - out[name] = '%s%s' % (shortcut, alias_str) + out[name] = f'{shortcut}{alias_str}' return out def show_shortcuts(self): @@ -501,14 +531,15 @@ def __contains__(self, name): return name in self._actions_dict def __repr__(self): - return ''.format(sorted(self._actions_dict)) + return f'' # ----------------------------------------------------------------------------- # Snippets # ----------------------------------------------------------------------------- -class Snippets(object): + +class Snippets: """Provide keyboard snippets to quickly execute actions from a GUI. This class attaches to a GUI and an `Actions` instance. To every command @@ -542,11 +573,11 @@ class Snippets(object): """ # HACK: Unicode characters do not seem to work on Python 2 - cursor = '\u200A\u258C' + cursor = '\u200a\u258c' # Allowed characters in snippet mode. # A Qt shortcut will be created for every character. - _snippet_chars = r"abcdefghijklmnopqrstuvwxyz0123456789 ,.;?!_-+~=*/\(){}[]<>&|" + _snippet_chars = r'abcdefghijklmnopqrstuvwxyz0123456789 ,.;?!_-+~=*/\(){}[]<>&|' def __init__(self, gui): self.gui = gui @@ -571,7 +602,7 @@ def command(self): msg = self.gui.status_message n = len(msg) n_cur = len(self.cursor) - return msg[:n - n_cur] + return msg[: n - n_cur] @command.setter def command(self, value): @@ -584,13 +615,13 @@ def _backspace(self): """Erase the last character in the snippet command.""" if self.command == ':': return - logger.log(5, "Snippet keystroke `Backspace`.") + logger.log(5, 'Snippet keystroke `Backspace`.') self.command = self.command[:-1] def _enter(self): """Disable the snippet mode and execute the command.""" command = self.command - logger.log(5, "Snippet keystroke `Enter`.") + logger.log(5, 'Snippet keystroke `Enter`.') # NOTE: we need to set back the actions (mode_off) before running # the command. self.mode_off() @@ -607,29 +638,27 @@ def _create_snippet_actions(self): def _make_func(char): def callback(): - logger.log(5, "Snippet keystroke `%s`.", char) + logger.log(5, 'Snippet keystroke `%s`.', char) self.command += char + return callback # Lowercase letters. - self.actions.add( - name='_snippet_{}'.format(i), - shortcut=char, - callback=_make_func(char)) + self.actions.add(name=f'_snippet_{i}', shortcut=char, callback=_make_func(char)) # Uppercase letters. if char in self._snippet_chars[:26]: self.actions.add( - name='_snippet_{}_upper'.format(i), - shortcut='shift+' + char, - callback=_make_func(char.upper())) + name=f'_snippet_{i}_upper', + shortcut=f'shift+{char}', + callback=_make_func(char.upper()), + ) + self.actions.add(name='_snippet_backspace', shortcut='backspace', callback=self._backspace) self.actions.add( - name='_snippet_backspace', shortcut='backspace', callback=self._backspace) - self.actions.add( - name='_snippet_activate', shortcut=('enter', 'return'), callback=self._enter) - self.actions.add( - name='_snippet_disable', shortcut='escape', callback=self.mode_off) + name='_snippet_activate', shortcut=('enter', 'return'), callback=self._enter + ) + self.actions.add(name='_snippet_disable', shortcut='escape', callback=self.mode_off) def run(self, snippet): """Execute a snippet command. @@ -642,7 +671,7 @@ def run(self, snippet): snippet_args = _parse_snippet(snippet) name = snippet_args[0] - logger.debug("Processing snippet `%s`.", snippet) + logger.debug('Processing snippet `%s`.', snippet) try: # Try to run the snippet on all attached Actions instances. for actions in self.gui.actions: @@ -655,7 +684,7 @@ def run(self, snippet): pass logger.warning("Couldn't find action `%s`.", name) except Exception as e: - logger.warning("Error when executing snippet: \"%s\".", str(e)) + logger.warning('Error when executing snippet: "%s".', str(e)) logger.debug(''.join(traceback.format_exception(*sys.exc_info()))) def is_mode_on(self): @@ -664,7 +693,7 @@ def is_mode_on(self): def mode_on(self): """Enable the snippet mode.""" - logger.debug("Snippet mode enabled, press `escape` to leave this mode.") + logger.debug('Snippet mode enabled, press `escape` to leave this mode.') # Save the current status message. self._status_message = self.gui.status_message self.gui.lock_status() diff --git a/phy/gui/gui.py b/phy/gui/gui.py index 947aac5ef..7d80129eb 100644 --- a/phy/gui/gui.py +++ b/phy/gui/gui.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """Qt dock window.""" @@ -7,17 +5,37 @@ # Imports # ----------------------------------------------------------------------------- +import logging from collections import defaultdict from functools import partial -import logging -from .qt import ( - QApplication, QWidget, QDockWidget, QHBoxLayout, QVBoxLayout, QPushButton, QLabel, QCheckBox, - QMenu, QToolBar, QStatusBar, QMainWindow, QMessageBox, Qt, QPoint, QSize, _load_font, - _wait, prompt, show_box, screenshot as make_screenshot) -from .state import GUIState, _gui_state_path, _get_default_state_path +from phylib.utils import connect, emit + from .actions import Actions, Snippets -from phylib.utils import emit, connect +from .qt import ( + QApplication, + QCheckBox, + QDockWidget, + QHBoxLayout, + QLabel, + QMainWindow, + QMenu, + QMessageBox, + QPoint, + QPushButton, + QSize, + QStatusBar, + Qt, + QToolBar, + QVBoxLayout, + QWidget, + _load_font, + _wait, + prompt, + show_box, +) +from .qt import screenshot as make_screenshot +from .state import GUIState, _get_default_state_path, _gui_state_path logger = logging.getLogger(__name__) @@ -26,11 +44,13 @@ # GUI utils # ----------------------------------------------------------------------------- + def _try_get_matplotlib_canvas(view): """Get the Qt widget from a matplotlib figure.""" try: - from matplotlib.pyplot import Figure from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg + from matplotlib.pyplot import Figure + if isinstance(view, Figure): view = FigureCanvasQTAgg(view) # Case where the view has a .figure property which is a matplotlib figure. @@ -39,13 +59,14 @@ def _try_get_matplotlib_canvas(view): elif isinstance(getattr(getattr(view, 'canvas', None), 'figure', None), Figure): view = FigureCanvasQTAgg(view.canvas.figure) except ImportError as e: # pragma: no cover - logger.warning("Import error: %s", e) + logger.warning('Import error: %s', e) return view def _try_get_opengl_canvas(view): """Convert from QOpenGLWindow to QOpenGLWidget.""" from phy.plot.base import BaseCanvas + if isinstance(view, BaseCanvas): return QWidget.createWindowContainer(view) elif isinstance(getattr(view, 'canvas', None), BaseCanvas): @@ -61,7 +82,7 @@ def _widget_position(widget): # pragma: no cover # Dock widget # ----------------------------------------------------------------------------- -DOCK_TITLE_STYLESHEET = ''' +DOCK_TITLE_STYLESHEET = """ * { padding: 0; margin: 0; @@ -95,10 +116,10 @@ def _widget_position(widget): # pragma: no cover QPushButton:checked { background: #6c717a; } -''' +""" -DOCK_STATUS_STYLESHEET = ''' +DOCK_STATUS_STYLESHEET = """ * { padding: 0; margin: 0; @@ -110,7 +131,7 @@ def _widget_position(widget): # pragma: no cover QLabel { padding: 3px; } -''' +""" class DockWidget(QDockWidget): @@ -126,7 +147,7 @@ class DockWidget(QDockWidget): max_status_length = 64 def __init__(self, *args, widget=None, **kwargs): - super(DockWidget, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) # Load the font awesome font. self._font = _load_font('fa-solid-900.ttf') self._dock_widgets = {} @@ -135,11 +156,18 @@ def __init__(self, *args, widget=None, **kwargs): def closeEvent(self, e): """Qt slot when the window is closed.""" emit('close_dock_widget', self) - super(DockWidget, self).closeEvent(e) + super().closeEvent(e) def add_button( - self, callback=None, text=None, icon=None, checkable=False, - checked=False, event=None, name=None): + self, + callback=None, + text=None, + icon=None, + checkable=False, + checked=False, + event=None, + name=None, + ): """Add a button to the dock title bar, to the right. Parameters @@ -166,8 +194,14 @@ def add_button( """ if callback is None: return partial( - self.add_button, text=text, icon=icon, name=name, - checkable=checkable, checked=checked, event=event) + self.add_button, + text=text, + icon=icon, + name=name, + checkable=checkable, + checked=checked, + event=event, + ) name = name or getattr(callback, '__name__', None) or text assert name @@ -180,12 +214,14 @@ def add_button( button.setToolTip(name) if callback: + @button.clicked.connect def on_clicked(state): return callback(state) # Change the state of the button when this event is called. if event: + @connect(event=event, sender=self.view) def on_state_changed(sender, checked): button.setChecked(checked) @@ -223,6 +259,7 @@ def add_checkbox(self, callback=None, text=None, checked=False, name=None): if checked: checkbox.setCheckState(Qt.Checked if checked else Qt.Unchecked) if callback: + @checkbox.stateChanged.connect def on_state_changed(state): return callback(state == Qt.Checked) @@ -246,7 +283,7 @@ def set_status(self, text): """Set the status text of the widget.""" n = self.max_status_length if len(text) >= n: - text = text[:n // 2] + ' ... ' + text[-n // 2:] + text = f'{text[: n // 2]} ... {text[-n // 2 :]}' self._status.setText(text) def _default_buttons(self): @@ -257,10 +294,17 @@ def _default_buttons(self): # Close button. @self.add_button(name='close', text='✕') def on_close(e): # pragma: no cover - if not self.confirm_before_close_view or show_box( - prompt( - "Close %s?" % self.windowTitle(), - buttons=['yes', 'no'], title='Close?')) == 'yes': + if ( + not self.confirm_before_close_view + or show_box( + prompt( + f'Close {self.windowTitle()}?', + buttons=['yes', 'no'], + title='Close?', + ) + ) + == 'yes' + ): self.close() # Screenshot button. @@ -282,7 +326,7 @@ def on_view_menu(e): # pragma: no cover def _create_menu(self): """Create the contextual menu for this view.""" - self._menu = QMenu("%s menu" % self.objectName(), self) + self._menu = QMenu(f'{self.objectName()} menu', self) def _create_title_bar(self): """Create the title bar.""" @@ -357,10 +401,10 @@ def _create_dock_widget(widget, name, closable=True, floatable=True): dock.setFeatures(options) dock.setAllowedAreas( - Qt.LeftDockWidgetArea | - Qt.RightDockWidgetArea | - Qt.TopDockWidgetArea | - Qt.BottomDockWidgetArea + Qt.LeftDockWidgetArea + | Qt.RightDockWidgetArea + | Qt.TopDockWidgetArea + | Qt.BottomDockWidgetArea ) dock._create_menu() @@ -371,11 +415,12 @@ def _create_dock_widget(widget, name, closable=True, floatable=True): def _get_dock_position(position): - return {'left': Qt.LeftDockWidgetArea, - 'right': Qt.RightDockWidgetArea, - 'top': Qt.TopDockWidgetArea, - 'bottom': Qt.BottomDockWidgetArea, - }[position or 'right'] + return { + 'left': Qt.LeftDockWidgetArea, + 'right': Qt.RightDockWidgetArea, + 'top': Qt.TopDockWidgetArea, + 'bottom': Qt.BottomDockWidgetArea, + }[position or 'right'] def _prompt_save(): # pragma: no cover @@ -385,8 +430,10 @@ def _prompt_save(): # pragma: no cover """ b = prompt( - "Do you want to save your changes before quitting?", - buttons=['save', 'cancel', 'close'], title='Save') + 'Do you want to save your changes before quitting?', + buttons=['save', 'cancel', 'close'], + title='Save', + ) return show_box(b) @@ -400,6 +447,7 @@ def _remove_duplicates(seq): # GUI main window # ----------------------------------------------------------------------------- + class GUI(QMainWindow): """A Qt main window containing docking widgets. This class derives from `QMainWindow`. @@ -447,20 +495,29 @@ class GUI(QMainWindow): has_save_action = True def __init__( - self, position=None, size=None, name=None, subtitle=None, view_creator=None, - view_count=None, default_views=None, config_dir=None, enable_threading=True, **kwargs): + self, + position=None, + size=None, + name=None, + subtitle=None, + view_creator=None, + view_count=None, + default_views=None, + config_dir=None, + enable_threading=True, + **kwargs, + ): # HACK to ensure that closeEvent is called only twice (seems like a # Qt bug). self._enable_threading = enable_threading self._closed = False if not QApplication.instance(): # pragma: no cover - raise RuntimeError("A Qt application must be created.") - super(GUI, self).__init__() - self.setDockOptions( - QMainWindow.AllowTabbedDocks | QMainWindow.AllowNestedDocks) + raise RuntimeError('A Qt application must be created.') + super().__init__() + self.setDockOptions(QMainWindow.AllowTabbedDocks | QMainWindow.AllowNestedDocks) self.setAnimated(False) - logger.debug("Creating GUI.") + logger.debug('Creating GUI.') self._set_name(name, str(subtitle or '')) position = position or (200, 200) @@ -476,28 +533,42 @@ def __init__( # Mapping {name: menuBar}. self._menus = {} ds = self.default_shortcuts - self.file_actions = Actions(self, name='File', menu='&File', default_shortcuts=ds) - self.view_actions = Actions(self, name='View', menu='&View', default_shortcuts=ds) - self.help_actions = Actions(self, name='Help', menu='&Help', default_shortcuts=ds) + self.file_actions = Actions( + self, name='File', menu='&File', default_shortcuts=ds + ) + self.view_actions = Actions( + self, name='View', menu='&View', default_shortcuts=ds + ) + self.help_actions = Actions( + self, name='Help', menu='&Help', default_shortcuts=ds + ) # Views, self._views = [] - self._view_class_indices = defaultdict(int) # Dictionary {view_name: next_usable_index} + self._view_class_indices = defaultdict( + int + ) # Dictionary {view_name: next_usable_index} # Create the GUI state. state_path = _gui_state_path(self.name, config_dir=config_dir) - default_state_path = kwargs.pop('default_state_path', _get_default_state_path(self)) - self.state = GUIState(state_path, default_state_path=default_state_path, **kwargs) + default_state_path = kwargs.pop( + 'default_state_path', _get_default_state_path(self) + ) + self.state = GUIState( + state_path, default_state_path=default_state_path, **kwargs + ) # View creator: dictionary {view_class: function_that_adds_view} self.default_views = default_views or () self.view_creator = view_creator or {} # View count: take the requested one, or the GUI state one. self._requested_view_count = ( - view_count if view_count is not None else self.state.get('view_count', {})) + view_count if view_count is not None else self.state.get('view_count', {}) + ) # If there is still no view count, use a default one. - self._requested_view_count = self._requested_view_count or { - view_name: 1 for view_name in default_views or ()} + self._requested_view_count = self._requested_view_count or dict.fromkeys( + default_views or (), 1 + ) # Status bar. self._lock_status = False @@ -516,7 +587,7 @@ def __init__( @connect(sender=self) def on_show(sender): - logger.debug("Load the geometry state.") + logger.debug('Load the geometry state.') gs = self.state.get('geometry_state', None) self.restore_geometry_state(gs) @@ -524,7 +595,7 @@ def _set_name(self, name, subtitle): """Set the GUI name.""" if name is None: name = self.__class__.__name__ - title = name if not subtitle else name + ' - ' + subtitle + title = name if not subtitle else f'{name} - {subtitle}' self.setWindowTitle(title) self.setObjectName(name) # Set the name in the GUI. @@ -542,6 +613,7 @@ def set_default_actions(self): # File menu. if self.has_save_action: + @self.file_actions.add(icon='f0c7', toolbar=True) def save(): emit('request_save', self) @@ -555,9 +627,10 @@ def exit(): for view_name in sorted(self.view_creator.keys()): self.view_actions.add( partial(self.create_and_add_view, view_name), - name='Add %s' % view_name, - docstring="Add %s" % view_name, - show_shortcut=False) + name=f'Add {view_name}', + docstring=f'Add {view_name}', + show_shortcut=False, + ) self.view_actions.separator() # Help menu. @@ -571,13 +644,15 @@ def show_all_shortcuts(): def about(): # pragma: no cover """Display an about dialog.""" from phy import __version_git__ - msg = "phy {} v{}".format(self.name, __version_git__) + + msg = f'phy {self.name} v{__version_git__}' try: from phylib import __version__ - msg += "\nphylib v{}".format(__version__) + + msg += f'\nphylib v{__version__}' except ImportError: pass - QMessageBox.about(self, "About", msg) + QMessageBox.about(self, 'About', msg) # Events # ------------------------------------------------------------------------- @@ -593,11 +668,11 @@ def closeEvent(self, e): if False in res: # pragma: no cover e.ignore() return - super(GUI, self).closeEvent(e) + super().closeEvent(e) self._closed = True # Save the state to disk when closing the GUI. - logger.debug("Save the geometry state.") + logger.debug('Save the geometry state.') gs = self.save_geometry_state() self.state['geometry_state'] = gs self.state['view_count'] = self.view_count @@ -605,7 +680,7 @@ def closeEvent(self, e): def show(self): """Show the window.""" - super(GUI, self).show() + super().show() emit('show', self) # Views @@ -631,8 +706,10 @@ def list_views(self, *classes): """Return the list of views which are instances of one or several classes.""" s = set(classes) return [ - view for view in self._views - if s.intersection({view.__class__, view.__class__.__name__})] + view + for view in self._views + if s.intersection({view.__class__, view.__class__.__name__}) + ] def get_view(self, cls, index=0): """Return a view from a given class. If there are multiple views of the same class, @@ -655,7 +732,7 @@ def _set_view_name(self, view): # index is the next usable index for the view's class. index = self._view_class_indices.get(cls, 0) assert index >= 1 - name = '%s (%d)' % (basename, index) + name = f'{basename} ({index})' view.name = name return name @@ -668,7 +745,7 @@ def create_and_add_view(self, view_name): # Create the view with the view creation function. view = fn() if view is None: # pragma: no cover - logger.warning("Could not create view %s.", view_name) + logger.warning('Could not create view %s.', view_name) return # Attach the view to the GUI if it has an attach(gui) method, # otherwise add the view. @@ -682,10 +759,17 @@ def create_views(self): """Create and add as many views as specified in view_count.""" self.view_actions.separator() # Keep the order of self.default_views. - view_names = [vn for vn in self.default_views if vn in self._requested_view_count] + view_names = [ + vn for vn in self.default_views if vn in self._requested_view_count + ] # We add the views in the requested view count, but not in the default views. - view_names.extend([ - vn for vn in self._requested_view_count.keys() if vn not in self.default_views]) + view_names.extend( + [ + vn + for vn in self._requested_view_count.keys() + if vn not in self.default_views + ] + ) # Remove duplicates in view names. view_names = _remove_duplicates(view_names) # We add the view in the order they appear in the default views. @@ -697,7 +781,9 @@ def create_views(self): for i in range(n_views): self.create_and_add_view(view_name) - def add_view(self, view, position=None, closable=True, floatable=True, floating=None): + def add_view( + self, view, position=None, closable=True, floatable=True, floating=None + ): """Add a dock widget to the main window. Parameters @@ -715,7 +801,7 @@ def add_view(self, view, position=None, closable=True, floatable=True, floating= """ - logger.debug("Add view %s to GUI.", view.__class__.__name__) + logger.debug('Add view %s to GUI.', view.__class__.__name__) name = self._set_view_name(view) self._views.append(view) @@ -739,7 +825,7 @@ def on_close_dock_widget(sender): emit('close_view', view, self) dock.show() - logger.log(5, "Add %s to GUI.", name) + logger.log(5, 'Add %s to GUI.', name) return dock # Menu bar @@ -752,7 +838,9 @@ def get_menu(self, name, insert_before=None): if not insert_before: self.menuBar().addMenu(menu) else: - self.menuBar().insertMenu(self.get_menu(insert_before).menuAction(), menu) + self.menuBar().insertMenu( + self.get_menu(insert_before).menuAction(), menu + ) self._menus[name] = menu return self._menus[name] @@ -823,6 +911,6 @@ def restore_geometry_state(self, gs): if not gs: return if gs.get('geometry', None): - self.restoreGeometry((gs['geometry'])) + self.restoreGeometry(gs['geometry']) if gs.get('state', None): - self.restoreState((gs['state'])) + self.restoreState(gs['state']) diff --git a/phy/gui/qt.py b/phy/gui/qt.py index c189335b3..415971fa8 100644 --- a/phy/gui/qt.py +++ b/phy/gui/qt.py @@ -1,23 +1,21 @@ -# -*- coding: utf-8 -*- - """Qt utilities.""" # ----------------------------------------------------------------------------- # Imports # ----------------------------------------------------------------------------- -from contextlib import contextmanager -from datetime import datetime -from functools import wraps, partial import logging import os import os.path as op -from pathlib import Path import shutil import sys import tempfile -from timeit import default_timer import traceback +from contextlib import contextmanager +from datetime import datetime +from functools import partial, wraps +from pathlib import Path +from timeit import default_timer logger = logging.getLogger(__name__) @@ -30,33 +28,72 @@ # https://riverbankcomputing.com/pipermail/pyqt/2014-January/033681.html from OpenGL import GL # noqa -from PyQt5.QtCore import (Qt, QByteArray, QMetaObject, QObject, # noqa - QVariant, QEventLoop, QTimer, QPoint, QTimer, - QThreadPool, QRunnable, - pyqtSignal, pyqtSlot, QSize, QUrl, - QEvent, QCoreApplication, - qInstallMessageHandler, - ) +from PyQt5.QtCore import ( + Qt, + QByteArray, + QMetaObject, + QObject, # noqa + QVariant, + QEventLoop, + QPoint, + QTimer, + QThreadPool, + QRunnable, + pyqtSignal, + pyqtSlot, + QSize, + QUrl, + QEvent, + QCoreApplication, + qInstallMessageHandler, +) from PyQt5.QtGui import ( # noqa - QKeySequence, QIcon, QColor, QMouseEvent, QGuiApplication, - QFontDatabase, QWindow, QOpenGLWindow) -from PyQt5.QtWebEngineWidgets import (QWebEngineView, # noqa - QWebEnginePage, - # QWebSettings, - ) + QKeySequence, + QIcon, + QColor, + QMouseEvent, + QGuiApplication, + QFontDatabase, + QWindow, + QOpenGLWindow, +) +from PyQt5.QtWebEngineWidgets import ( + QWebEngineView, # noqa + QWebEnginePage, + # QWebSettings, +) from PyQt5.QtWebChannel import QWebChannel # noqa -from PyQt5.QtWidgets import (# noqa - QAction, QStatusBar, QMainWindow, QDockWidget, QToolBar, - QWidget, QHBoxLayout, QVBoxLayout, QGridLayout, QScrollArea, - QPushButton, QLabel, QCheckBox, QPlainTextEdit, - QLineEdit, QSlider, QSpinBox, QDoubleSpinBox, - QMessageBox, QApplication, QMenu, QMenuBar, - QInputDialog, QOpenGLWidget) +from PyQt5.QtWidgets import ( # noqa + QAction, + QStatusBar, + QMainWindow, + QDockWidget, + QToolBar, + QWidget, + QHBoxLayout, + QVBoxLayout, + QGridLayout, + QScrollArea, + QPushButton, + QLabel, + QCheckBox, + QPlainTextEdit, + QLineEdit, + QSlider, + QSpinBox, + QDoubleSpinBox, + QMessageBox, + QApplication, + QMenu, + QMenuBar, + QInputDialog, + QOpenGLWidget, +) # Enable high DPI support. # BUG: uncommenting this create scaling bugs on high DPI screens # on Ubuntu. -#QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) +# QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) # ----------------------------------------------------------------------------- @@ -68,11 +105,13 @@ def mockable(f): """Wrap interactive Qt functions that should be mockable in the testing suite.""" + @wraps(f) def wrapped(*args, **kwargs): if _MOCK is not None: return _MOCK return f(*args, **kwargs) + return wrapped @@ -115,12 +154,14 @@ def require_qt(func): the case. """ + @wraps(func) def wrapped(*args, **kwargs): if not QApplication.instance(): # pragma: no cover - logger.warning("Creating a Qt application.") + logger.warning('Creating a Qt application.') create_app() return func(*args, **kwargs) + return wrapped @@ -135,6 +176,7 @@ def run_app(): # pragma: no cover # Internal utility functions # ----------------------------------------------------------------------------- + @mockable def _button_enum_from_name(name): return getattr(QMessageBox, name.capitalize()) @@ -174,26 +216,28 @@ def _block(until_true, timeout=None): while not until_true() and (default_timer() - t0 < timeout): app = QApplication.instance() - app.processEvents(QEventLoop.AllEvents, - int(timeout * 1000)) + app.processEvents(QEventLoop.AllEvents, int(timeout * 1000)) if not until_true(): - logger.error("Timeout in _block().") + logger.error('Timeout in _block().') # NOTE: make sure we remove any busy cursor. app.restoreOverrideCursor() app.restoreOverrideCursor() - raise RuntimeError("Timeout in _block().") + raise RuntimeError('Timeout in _block().') def _wait(ms): """Wait for a given number of milliseconds, without blocking the GUI.""" from PyQt5 import QtTest + QtTest.QTest.qWait(ms) def _debug_trace(): # pragma: no cover """Set a tracepoint in the Python debugger that works with Qt.""" - from PyQt5.QtCore import pyqtRemoveInputHook from pdb import set_trace + + from PyQt5.QtCore import pyqtRemoveInputHook + pyqtRemoveInputHook() set_trace() @@ -217,6 +261,7 @@ def _load_font(name, size=8): # Public functions # ----------------------------------------------------------------------------- + @mockable def prompt(message, buttons=('yes', 'no'), title='Question'): """Display a dialog with several buttons to confirm or cancel an action. @@ -235,7 +280,7 @@ def prompt(message, buttons=('yes', 'no'), title='Question'): """ buttons = [(button, _button_enum_from_name(button)) for button in buttons] arg_buttons = 0 - for (_, button) in buttons: + for _, button in buttons: arg_buttons |= button box = QMessageBox() box.setWindowTitle(title) @@ -262,6 +307,7 @@ def message_box(message, title='Message', level=None): # pragma: no cover class QtDialogLogger(logging.Handler): """Display a message box for all errors.""" + def emit(self, record): # pragma: no cover msg = self.format(record) message_box(msg, title='An error has occurred', level='critical') @@ -315,8 +361,9 @@ def busy_cursor(activate=True): def screenshot_default_path(widget, dir=None): """Return a default path for the screenshot of a widget.""" from phylib.utils._misc import phy_config_dir + date = datetime.now().strftime('%Y%m%d%H%M%S') - name = 'phy_screenshot_%s_%s.png' % (date, widget.__class__.__name__) + name = f'phy_screenshot_{date}_{widget.__class__.__name__}.png' path = (Path(dir) if dir else phy_config_dir() / 'screenshots') / name path.parent.mkdir(exist_ok=True, parents=True) return path @@ -344,7 +391,7 @@ def screenshot(widget, path=None, dir=None): else: # Generic call for regular Qt widgets. widget.grab().save(str(path)) - logger.info("Saved screenshot to %s.", path) + logger.info('Saved screenshot to %s.', path) return path @@ -374,37 +421,46 @@ def _get_icon(icon, size=64, color='black'): # from https://github.com/Pythonity/icon-font-to-png/blob/master/icon_font_to_png/icon_font.py static_dir = op.join(op.dirname(op.abspath(__file__)), 'static/icons/') ttf_file = op.abspath(op.join(static_dir, '../fa-solid-900.ttf')) - output_path = op.join(static_dir, icon + '.png') + output_path = op.join(static_dir, f'{icon}.png') if not op.exists(output_path): # pragma: no cover # Ideally, this should only run on the developer's machine. - logger.debug("Saving icon `%s` using the PIL library.", output_path) + logger.debug('Saving icon `%s` using the PIL library.', output_path) from PIL import Image, ImageDraw, ImageFont + org_size = size size = max(150, size) - image = Image.new("RGBA", (size, size), color=(0, 0, 0, 0)) + image = Image.new('RGBA', (size, size), color=(0, 0, 0, 0)) draw = ImageDraw.Draw(image) font = ImageFont.truetype(ttf_file, int(size)) width, height = draw.textsize(hex_icon, font=font) draw.text( - (float(size - width) / 2, float(size - height) / 2), hex_icon, font=font, fill=color) + (float(size - width) / 2, float(size - height) / 2), + hex_icon, + font=font, + fill=color, + ) # Get bounding box bbox = image.getbbox() # Create an alpha mask - image_mask = Image.new("L", (size, size), 0) + image_mask = Image.new('L', (size, size), 0) draw_mask = ImageDraw.Draw(image_mask) # Draw the icon on the mask draw_mask.text( - (float(size - width) / 2, float(size - height) / 2), hex_icon, font=font, fill=255) + (float(size - width) / 2, float(size - height) / 2), + hex_icon, + font=font, + fill=255, + ) # Create a solid color image and apply the mask - icon_image = Image.new("RGBA", (size, size), color) + icon_image = Image.new('RGBA', (size, size), color) icon_image.putalpha(image_mask) if bbox: @@ -414,7 +470,7 @@ def _get_icon(icon, size=64, color='black'): border_h = int((size - (bbox[3] - bbox[1])) / 2) # Create output image - out_image = Image.new("RGBA", (size, size), (0, 0, 0, 0)) + out_image = Image.new('RGBA', (size, size), (0, 0, 0, 0)) out_image.paste(icon_image, (border_w, border_h)) # If necessary, scale the image to the target size @@ -433,6 +489,7 @@ def _get_icon(icon, size=64, color='black'): # Widgets # ----------------------------------------------------------------------------- + def _static_abs_path(rel_path): """Return the absolute path of a static file saved in this repository.""" return Path(__file__).parent / 'static' / rel_path @@ -440,11 +497,12 @@ def _static_abs_path(rel_path): class WebPage(QWebEnginePage): """A Qt web page widget.""" + _raise_on_javascript_error = False def javaScriptConsoleMessage(self, level, msg, line, source): - super(WebPage, self).javaScriptConsoleMessage(level, msg, line, source) - msg = "[JS:L%02d] %s" % (line, msg) + super().javaScriptConsoleMessage(level, msg, line, source) + msg = f'[JS:L{line:02d}] {msg}' f = (partial(logger.log, 5), logger.warning, logger.error)[level] if self._raise_on_javascript_error and level >= 2: raise RuntimeError(msg) @@ -455,7 +513,7 @@ class WebView(QWebEngineView): """A generic HTML widget.""" def __init__(self, *args): - super(WebView, self).__init__(*args) + super().__init__(*args) self.html = None assert isinstance(self.window(), QWidget) self._page = WebPage(self) @@ -467,7 +525,7 @@ def set_html(self, html, callback=None): """Set the HTML code.""" self._callback = callback self.loadFinished.connect(self._loadFinished) - static_dir = str(Path(__file__).parent / 'static') + '/' + static_dir = f'{str(Path(__file__).parent / "static")}/' # Create local file from HTML self.clear_temporary_files() @@ -497,8 +555,10 @@ def _loadFinished(self, result): # Threading # ----------------------------------------------------------------------------- + class WorkerSignals(QObject): """Object holding some signals for the workers.""" + finished = pyqtSignal() error = pyqtSignal(tuple) result = pyqtSignal(object) @@ -530,8 +590,9 @@ class Worker(QRunnable): **kwargs : function keyword arguments """ + def __init__(self, fn, *args, **kwargs): - super(Worker, self).__init__() + super().__init__() self.fn = fn self.args = args self.kwargs = kwargs @@ -555,7 +616,7 @@ def run(self): # pragma: no cover self.signals.finished.emit() -class Debouncer(object): +class Debouncer: """Debouncer to work in a Qt application. Jobs are submitted at given times. They are executed immediately if the @@ -592,7 +653,9 @@ class Debouncer(object): def __init__(self, delay=None): self.delay = delay or self.delay # minimum delay between job executions, in ms. self._last_submission_time = 0 - self.is_waiting = False # whether we're already waiting for the end of the interactions + self.is_waiting = ( + False # whether we're already waiting for the end of the interactions + ) self.pending_functions = {} # assign keys to pending functions. self._timer = QTimer() self._timer.timeout.connect(self._timer_callback) @@ -600,12 +663,12 @@ def __init__(self, delay=None): def _elapsed_enough(self): """Return whether the elapsed time since the last submission is greater than the threshold.""" - return default_timer() - self._last_submission_time > self.delay * .001 + return default_timer() - self._last_submission_time > self.delay * 0.001 def _timer_callback(self): """Callback for the timer.""" if self._elapsed_enough(): - logger.log(self._log_level, "Stop waiting and triggering.") + logger.log(self._log_level, 'Stop waiting and triggering.') self._timer.stop() self.trigger() @@ -614,12 +677,12 @@ def submit(self, f, *args, key=None, **kwargs): is higher than the threshold, or wait until executing it otherwiser.""" self.pending_functions[key] = (f, args, kwargs) if self._elapsed_enough(): - logger.log(self._log_level, "Triggering action immediately.") + logger.log(self._log_level, 'Triggering action immediately.') # Trigger the action immediately if the delay since the last submission is greater # than the threshold. self.trigger() else: - logger.log(self._log_level, "Waiting...") + logger.log(self._log_level, 'Waiting...') # Otherwise, we start the timer. if not self._timer.isActive(): self._timer.start(25) @@ -631,18 +694,19 @@ def trigger(self): if item is None: continue f, args, kwargs = item - logger.log(self._log_level, "Trigger %s.", f.__name__) + logger.log(self._log_level, 'Trigger %s.', f.__name__) f(*args, **kwargs) self.pending_functions[key] = None - def stop_waiting(self, delay=.1): + def stop_waiting(self, delay=0.1): """Stop waiting and force the pending actions to execute (almost) immediately.""" # The trigger will occur in `delay` seconds. - self._last_submission_time = default_timer() - (self.delay * .001 - delay) + self._last_submission_time = default_timer() - (self.delay * 0.001 - delay) -class AsyncCaller(object): +class AsyncCaller: """Call a Python function after a delay.""" + def __init__(self, delay=10): self._delay = delay self._timer = None diff --git a/phy/gui/state.py b/phy/gui/state.py index 832262fce..b488ca867 100644 --- a/phy/gui/state.py +++ b/phy/gui/state.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """Qt dock window.""" @@ -10,15 +8,16 @@ try: # pragma: no cover from collections.abc import Mapping # noqa except ImportError: # pragma: no cover - from collections import Mapping # noqa -from copy import deepcopy + from collections.abc import Mapping # noqa import inspect import json import logging -from pathlib import Path import shutil +from copy import deepcopy +from pathlib import Path from phylib.utils import Bunch, _bunchify, load_json, save_json + from phy.utils import ensure_dir_exists, phy_config_dir logger = logging.getLogger(__name__) @@ -28,6 +27,7 @@ # GUI state # ----------------------------------------------------------------------------- + def _get_default_state_path(gui): """Return the path to the default state.json for a given GUI.""" gui_path = Path(inspect.getfile(gui.__class__)) @@ -43,10 +43,10 @@ def _gui_state_path(gui_name, config_dir=None): def _load_state(path): """Load a GUI state from a JSON file.""" try: - logger.debug("Load %s for GUIState.", path) + logger.debug('Load %s for GUIState.', path) data = load_json(str(path)) except json.decoder.JSONDecodeError as e: # pragma: no cover - logger.warning("Error decoding JSON: %s", e) + logger.warning('Error decoding JSON: %s', e) data = {} return _bunchify(data) @@ -57,7 +57,8 @@ def _filter_nested_dict(value, key=None, search_terms=None): # key is None for the root only. # Expression used to test whether we keep a key or not. keep = lambda k: k is None or ( - (not search_terms or k in search_terms) and not k.startswith('_')) + (not search_terms or k in search_terms) and not k.startswith('_') + ) # Process leaves. if not isinstance(value, Mapping): return value if keep(key) else None @@ -133,9 +134,11 @@ class GUIState(Bunch): in the local state, and not the global state. """ + def __init__( - self, path=None, local_path=None, default_state_path=None, local_keys=None, **kwargs): - super(GUIState, self).__init__(**kwargs) + self, path=None, local_path=None, default_state_path=None, local_keys=None, **kwargs + ): + super().__init__(**kwargs) self._path = Path(path) if path else None if self._path: ensure_dir_exists(str(self._path.parent)) @@ -168,19 +171,21 @@ def update_view_state(self, view, state): if name not in self: self[name] = Bunch() self[name].update(state) - logger.debug("Update GUI state for %s", name) + logger.debug('Update GUI state for %s', name) def _copy_default_state(self): """Copy the default GUI state to the user directory.""" if self._default_state_path and self._default_state_path.exists(): logger.debug( - "The GUI state file `%s` doesn't exist, creating a default one...", self._path) + "The GUI state file `%s` doesn't exist, creating a default one...", self._path + ) shutil.copy(self._default_state_path, self._path) - logger.info("Copied %s to %s.", self._default_state_path, self._path) + logger.info('Copied %s to %s.', self._default_state_path, self._path) elif self._default_state_path: # pragma: no cover logger.warning( - "Could not copy non-existing default state file %s.", self._default_state_path) + 'Could not copy non-existing default state file %s.', self._default_state_path + ) def add_local_keys(self, keys): """Add local keys.""" @@ -215,7 +220,7 @@ def _local_data(self): def _save_global(self): """Save the entire GUIState to the global file.""" path = self._path - logger.debug("Save global GUI state to `%s`.", path) + logger.debug('Save global GUI state to `%s`.', path) save_json(str(path), self._global_data) def _save_local(self): @@ -229,7 +234,7 @@ def _save_local(self): return assert self._local_path - logger.debug("Save local GUI state to `%s`.", path) + logger.debug('Save local GUI state to `%s`.', path) save_json(str(path), self._local_data) def save(self): diff --git a/phy/gui/tests/conftest.py b/phy/gui/tests/conftest.py index f2d836bc4..106ab75c7 100644 --- a/phy/gui/tests/conftest.py +++ b/phy/gui/tests/conftest.py @@ -1,20 +1,18 @@ -# -*- coding: utf-8 -*- - """Test gui.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ from pytest import fixture from ..actions import Actions, Snippets from ..gui import GUI - -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Utilities and fixtures -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + @fixture def gui(tempdir, qtbot): diff --git a/phy/gui/tests/test_actions.py b/phy/gui/tests/test_actions.py index c49724239..39e3e8db2 100644 --- a/phy/gui/tests/test_actions.py +++ b/phy/gui/tests/test_actions.py @@ -1,25 +1,29 @@ -# -*- coding: utf-8 -*- - """Test dock.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ from functools import partial +from phylib.utils.testing import captured_logging, captured_output from pytest import raises from ..actions import ( - _show_shortcuts, _show_snippets, _get_shortcut_string, _get_qkeysequence, _parse_snippet, - _expected_args, Actions) -from phylib.utils.testing import captured_output, captured_logging + Actions, + _expected_args, + _get_qkeysequence, + _get_shortcut_string, + _parse_snippet, + _show_shortcuts, + _show_snippets, +) from ..qt import mock_dialogs - -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test actions -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_expected_args(): assert _expected_args(lambda: 0) == () @@ -79,7 +83,6 @@ def test_actions_default_shortcuts(gui): def test_actions_simple(actions): - _res = [] def _action(*args): @@ -113,16 +116,17 @@ def show_my_shortcuts(): assert '%s' % text + return view.html == f'{text}' view.set_html('hello', _assert) qtbot.addWidget(view) @@ -149,7 +165,7 @@ def _assert(text): qtbot.waitForWindowShown(view) _block(lambda: _assert('hello')) - view.set_html("world") + view.set_html('world') _block(lambda: _assert('world')) view.close() @@ -163,7 +179,7 @@ def test_javascript_1(qtbot): qtbot.waitForWindowShown(view) _block(lambda: view.html is not None) view.close() - assert buf.getvalue() == "" + assert buf.getvalue() == '' def test_javascript_2(qtbot): @@ -180,7 +196,6 @@ def test_javascript_2(qtbot): def test_screenshot(qtbot, tempdir): - path = tempdir / 'capture.png' view = WebView() assert str(screenshot_default_path(view, dir=tempdir)).startswith(str(tempdir)) @@ -193,11 +208,10 @@ def test_screenshot(qtbot, tempdir): def test_prompt(qtbot): - assert _button_name_from_enum(QMessageBox.Save) == 'save' assert _button_enum_from_name('save') == QMessageBox.Save - box = prompt("How are you doing?", buttons=['save', 'cancel', 'close']) + box = prompt('How are you doing?', buttons=['save', 'cancel', 'close']) qtbot.mouseClick(box.buttons()[0], Qt.LeftButton) assert 'save' in str(box.clickedButton().text()).lower() diff --git a/phy/gui/tests/test_state.py b/phy/gui/tests/test_state.py index 298984ccd..5fba89abe 100644 --- a/phy/gui/tests/test_state.py +++ b/phy/gui/tests/test_state.py @@ -1,39 +1,40 @@ -# -*- coding: utf-8 -*- - """Test gui.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import logging import os import shutil -from ..state import GUIState, _gui_state_path, _get_default_state_path from phylib.utils import Bunch, load_json, save_json +from ..state import GUIState, _get_default_state_path, _gui_state_path + logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test GUI state -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + -class MyClass(object): +class MyClass: pass def test_get_default_state_path(): assert str(_get_default_state_path(MyClass())).endswith( - os.sep.join(('gui', 'tests', 'static', 'state.json'))) + os.sep.join(('gui', 'tests', 'static', 'state.json')) + ) def test_gui_state_view_1(tempdir): view = Bunch(name='MyView0') path = _gui_state_path('GUI', tempdir) state = GUIState(path) - state.update_view_state(view, dict(hello='world')) + state.update_view_state(view, {'hello': 'world'}) assert not state.get_view_state(Bunch(name='MyView')) assert not state.get_view_state(Bunch(name='MyView (1)')) assert state.get_view_state(view) == Bunch(hello='world') @@ -44,7 +45,7 @@ def test_gui_state_view_1(tempdir): shutil.copy(state._path, default_path) state._path.unlink() - logger.info("Create new GUI state.") + logger.info('Create new GUI state.') # The default state.json should be automatically copied and loaded. state = GUIState(path, default_state_path=default_path) assert state.MyView0.hello == 'world' diff --git a/phy/gui/tests/test_widgets.py b/phy/gui/tests/test_widgets.py index 4ce0eae3f..9e84775fa 100644 --- a/phy/gui/tests/test_widgets.py +++ b/phy/gui/tests/test_widgets.py @@ -1,25 +1,25 @@ -# -*- coding: utf-8 -*- - """Test widgets.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ from functools import partial from pathlib import Path -from pytest import fixture, mark from phylib.utils import connect, unconnect from phylib.utils.testing import captured_logging +from pytest import fixture, mark + import phy -from .test_qt import _block -from ..widgets import HTMLWidget, Table, Barrier, IPythonView, KeyValueWidget +from ..widgets import Barrier, HTMLWidget, IPythonView, KeyValueWidget, Table +from .test_qt import _block -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Fixtures -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _assert(f, expected): _out = [] @@ -39,16 +39,17 @@ def _wait_until_table_ready(qtbot, table): @fixture def table(qtbot): - columns = ["id", "count"] - data = [{"id": i, - "count": 100 - 10 * i, - "float": float(i), - "is_masked": True if i in (2, 3, 5) else False, - } for i in range(10)] - table = Table( - columns=columns, - value_names=['id', 'count', {'data': ['is_masked']}], - data=data) + columns = ['id', 'count'] + data = [ + { + 'id': i, + 'count': 100 - 10 * i, + 'float': float(i), + 'is_masked': i in (2, 3, 5), + } + for i in range(10) + ] + table = Table(columns=columns, value_names=['id', 'count', {'data': ['is_masked']}], data=data) _wait_until_table_ready(qtbot, table) yield table @@ -56,9 +57,10 @@ def table(qtbot): table.close() -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test widgets -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_widget_empty(qtbot): widget = HTMLWidget() @@ -117,7 +119,7 @@ def _callback(res): widget.close() -@mark.parametrize("event_name", ('select', 'nodebounce')) +@mark.parametrize('event_name', ('select', 'nodebounce')) def test_widget_javascript_debounce(qtbot, event_name): phy.gui.qt.Debouncer.delay = 300 @@ -128,15 +130,18 @@ def test_widget_javascript_debounce(qtbot, event_name): qtbot.waitForWindowShown(widget) _block(lambda: widget.html is not None) - event_code = lambda i: r''' - var event = new CustomEvent("phy_event", {detail: {name: '%s', data: {'i': %s}}}); + event_code = ( + lambda i: rf""" + var event = new CustomEvent("phy_event", {{detail: {{name: '{event_name}', data: {{'i': {i}}}}}}}); document.dispatchEvent(event); - ''' % (event_name, i) + """ + ) _l = [] def f(sender, *args): _l.append(args) + connect(f, sender=widget, event=event_name) for i in range(5): @@ -152,9 +157,10 @@ def f(sender, *args): phy.gui.qt.Debouncer.delay = 1 -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test key value widget -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_key_value_1(qtbot): widget = KeyValueWidget() @@ -163,29 +169,35 @@ def test_key_value_1(qtbot): qtbot.addWidget(widget) qtbot.waitForWindowShown(widget) - widget.add_pair("my text", "some text") - widget.add_pair("my text multiline", "some\ntext", 'multiline') - widget.add_pair("my float", 3.5) - widget.add_pair("my int", 3) - widget.add_pair("my bool", True) - widget.add_pair("my list", [1, 5]) + widget.add_pair('my text', 'some text') + widget.add_pair('my text multiline', 'some\ntext', 'multiline') + widget.add_pair('my float', 3.5) + widget.add_pair('my int', 3) + widget.add_pair('my bool', True) + widget.add_pair('my list', [1, 5]) widget.get_widget('my bool').setChecked(False) widget.get_widget('my list[0]').setValue(2) assert widget.to_dict() == { - 'my text': 'some text', 'my text multiline': 'some\ntext', - 'my float': 3.5, 'my int': 3, 'my bool': False, 'my list': [2, 5]} + 'my text': 'some text', + 'my text multiline': 'some\ntext', + 'my float': 3.5, + 'my int': 3, + 'my bool': False, + 'my list': [2, 5], + } # qtbot.stop() widget.close() -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test IPython view -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + -@mark.filterwarnings("ignore") +@mark.filterwarnings('ignore') def test_ipython_view_1(qtbot): view = IPythonView() view.show() @@ -195,9 +207,10 @@ def test_ipython_view_1(qtbot): view.close() -@mark.filterwarnings("ignore") +@mark.filterwarnings('ignore') def test_ipython_view_2(qtbot, tempdir): from ..gui import GUI + gui = GUI(config_dir=tempdir) gui.set_default_actions() @@ -213,9 +226,10 @@ def test_ipython_view_2(qtbot, tempdir): qtbot.wait(10) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test table -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_barrier_1(qtbot, table): table.select([1]) @@ -254,7 +268,6 @@ def test_table_0(qtbot, table): def test_table_1(qtbot, table): - assert table.is_ready() table.select([1, 2]) @@ -373,7 +386,7 @@ def test_table_remove_all_and_add_1(qtbot, table): def test_table_remove_all_and_add_2(qtbot, table): - table.remove_all_and_add({"id": 1000}) + table.remove_all_and_add({'id': 1000}) _assert(table.get_ids, [1000]) @@ -406,10 +419,10 @@ def test_table_change_and_sort_2(qtbot, table): def test_table_filter(qtbot, table): - table.filter("id == 5") + table.filter('id == 5') _assert(table.get_ids, [5]) - table.filter("count == 80") + table.filter('count == 80') _assert(table.get_ids, [2]) table.filter() diff --git a/phy/gui/widgets.py b/phy/gui/widgets.py index 8a3131115..ea0bec97b 100644 --- a/phy/gui/widgets.py +++ b/phy/gui/widgets.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """HTML widgets for GUIs.""" @@ -11,17 +9,31 @@ import logging from functools import partial -from qtconsole.rich_jupyter_widget import RichJupyterWidget +from phylib.utils import connect, emit +from phylib.utils._misc import _CustomEncoder, _pretty_floats, read_text +from phylib.utils._types import _is_integer from qtconsole.inprocess import QtInProcessKernelManager +from qtconsole.rich_jupyter_widget import RichJupyterWidget + +from phy.utils.color import _is_bright, colormaps from .qt import ( - WebView, QObject, QWebChannel, QWidget, QGridLayout, QPlainTextEdit, - QLabel, QLineEdit, QCheckBox, QSpinBox, QDoubleSpinBox, - pyqtSlot, _static_abs_path, _block, Debouncer) -from phylib.utils import emit, connect -from phy.utils.color import colormaps, _is_bright -from phylib.utils._misc import _CustomEncoder, read_text, _pretty_floats -from phylib.utils._types import _is_integer + Debouncer, + QCheckBox, + QDoubleSpinBox, + QGridLayout, + QLabel, + QLineEdit, + QObject, + QPlainTextEdit, + QSpinBox, + QWebChannel, + QWidget, + WebView, + _block, + _static_abs_path, + pyqtSlot, +) logger = logging.getLogger(__name__) @@ -30,16 +42,17 @@ # IPython widget # ----------------------------------------------------------------------------- + class IPythonView(RichJupyterWidget): """A view with an IPython console living in the same Python process as the GUI.""" def __init__(self, *args, **kwargs): - super(IPythonView, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def start_kernel(self): """Start the IPython kernel.""" - logger.debug("Starting the kernel.") + logger.debug('Starting the kernel.') self.kernel_manager = QtInProcessKernelManager() self.kernel_manager.start_kernel(show_banner=False) @@ -51,18 +64,22 @@ def start_kernel(self): self.kernel_client = self.kernel_manager.client() self.kernel_client.start_channels() except Exception as e: # pragma: no cover - logger.error("Could not start IPython kernel: %s.", str(e)) + logger.error('Could not start IPython kernel: %s.', str(e)) self.set_default_style('linux') self.exit_requested.connect(self.stop) def inject(self, **kwargs): """Inject variables into the IPython namespace.""" - logger.debug("Injecting variables into the kernel: %s.", ', '.join(kwargs.keys())) + logger.debug( + 'Injecting variables into the kernel: %s.', ', '.join(kwargs.keys()) + ) try: self.kernel.shell.push(kwargs) except Exception as e: # pragma: no cover - logger.error("Could not inject variables to the IPython kernel: %s.", str(e)) + logger.error( + 'Could not inject variables to the IPython kernel: %s.', str(e) + ) def attach(self, gui, **kwargs): """Add the view to the GUI, start the kernel, and inject the specified variables.""" @@ -71,11 +88,13 @@ def attach(self, gui, **kwargs): self.inject(gui=gui, **kwargs) try: import numpy + self.inject(np=numpy) except ImportError: # pragma: no cover pass try: import matplotlib.pyplot as plt + self.inject(plt=plt) except ImportError: # pragma: no cover pass @@ -86,12 +105,12 @@ def on_close_view(view, gui): def stop(self): """Stop the kernel.""" - logger.debug("Stopping the kernel.") + logger.debug('Stopping the kernel.') try: self.kernel_client.stop_channels() self.kernel_manager.shutdown_kernel() except Exception as e: # pragma: no cover - logger.error("Could not stop the IPython kernel: %s.", str(e)) + logger.error('Could not stop the IPython kernel: %s.', str(e)) # ----------------------------------------------------------------------------- @@ -165,7 +184,7 @@ def _uniq(seq): return [int(x) for x in seq if not (x in seen or seen_add(x))] -class Barrier(object): +class Barrier: """Implement a synchronization barrier.""" def __init__(self): @@ -199,7 +218,7 @@ def result(self, key): return self._results.get(key, None) -class HTMLBuilder(object): +class HTMLBuilder: """Build an HTML widget.""" def __init__(self, title=''): @@ -210,19 +229,19 @@ def __init__(self, title=''): def add_style(self, s): """Add a CSS style.""" - self.add_header(''.format(s)) + self.add_header(f'') def add_style_src(self, filename): """Add a link to a stylesheet URL.""" - self.add_header(('').format(filename)) + self.add_header(f'') def add_script(self, s): """Add Javascript code.""" - self.add_header(''.format(s)) + self.add_header(f'') def add_script_src(self, filename): """Add a link to a Javascript file.""" - self.add_header(''.format(filename)) + self.add_header(f'') def add_header(self, s): """Add HTML headers.""" @@ -252,16 +271,17 @@ def html(self): class JSEventEmitter(QObject): """Object used to relay the Javascript events to Python. Some vents can be debounced so that there is a minimal delay between two consecutive events of the same type.""" + _parent = None def __init__(self, *args, debounce_events=()): - super(JSEventEmitter, self).__init__(*args) + super().__init__(*args) self._debouncer = Debouncer() self._debounce_events = debounce_events @pyqtSlot(str, str) def emitJS(self, name, arg_json): - logger.log(5, "Emit from Python %s %s.", name, arg_json) + logger.log(5, 'Emit from Python %s %s.', name, arg_json) args = str(name), self._parent, json.loads(str(arg_json)) # NOTE: debounce some events but not other events coming from JS. # This is typically used for select events of table widgets. @@ -286,6 +306,7 @@ class HTMLWidget(WebView): The list of event names, raised by the underlying HTML widget, that should be debounced. """ + def __init__(self, *args, title='', debounce_events=()): # Due to a limitation of QWebChannel, need to register a Python object # BEFORE this web view is created?! @@ -294,7 +315,7 @@ def __init__(self, *args, title='', debounce_events=()): self.channel = QWebChannel(*args) self.channel.registerObject('eventEmitter', self._event) - super(HTMLWidget, self).__init__(*args) + super().__init__(*args) self.page().setWebChannel(self.channel) self.builder = HTMLBuilder(title=title) @@ -313,7 +334,8 @@ def build(self, callback=None): def view_source(self, callback=None): """View the HTML source of the widget.""" return self.eval_js( - "document.getElementsByTagName('html')[0].innerHTML", callback=callback) + "document.getElementsByTagName('html')[0].innerHTML", callback=callback + ) # Javascript methods # ------------------------------------------------------------------------- @@ -331,7 +353,7 @@ def eval_js(self, expr, callback=None): evaluated. It takes as input the output of the Javascript expression. """ - logger.log(5, "%s eval JS %s", self.__class__.__name__, expr) + logger.log(5, f'{self.__class__.__name__} eval JS {expr}') return self.page().runJavaScript(expr, callback or (lambda _: _)) @@ -339,6 +361,7 @@ def eval_js(self, expr, callback=None): # HTML table # ----------------------------------------------------------------------------- + def dumps(o): """Dump a JSON object into a string, with pretty floats.""" return json.dumps(_pretty_floats(o), cls=_CustomEncoder) @@ -347,13 +370,14 @@ def dumps(o): def _color_styles(): """Use colormap colors in table widget.""" return '\n'.join( - ''' - #table .color-%d > td[class='id'] { - background-color: rgb(%d, %d, %d); - %s - } - ''' % (i, r, g, b, 'color: #000 !important;' if _is_bright((r, g, b)) else '') - for i, (r, g, b) in enumerate(colormaps.default * 255)) + f""" + #table .color-{i} > td[class='id'] {{ + background-color: rgb({r}, {g}, {b}); + {'color: #000 !important;' if _is_bright((r, g, b)) else ''} + }} + """ + for i, (r, g, b) in enumerate(colormaps.default * 255) + ) class Table(HTMLWidget): @@ -367,9 +391,16 @@ class Table(HTMLWidget): _ready = False def __init__( - self, *args, columns=None, value_names=None, data=None, sort=None, title='', - debounce_events=()): - super(Table, self).__init__(*args, title=title, debounce_events=debounce_events) + self, + *args, + columns=None, + value_names=None, + data=None, + sort=None, + title='', + debounce_events=(), + ): + super().__init__(*args, title=title, debounce_events=debounce_events) self._init_table(columns=columns, value_names=value_names, data=data, sort=sort) def eval_js(self, expr, callback=None): @@ -396,8 +427,8 @@ def eval_js(self, expr, callback=None): """ # Avoid JS errors when the table is not yet fully loaded. - expr = 'if (typeof table !== "undefined") ' + expr - return super(Table, self).eval_js(expr, callback=callback) + expr = f'if (typeof table !== "undefined") {expr}' + return super().eval_js(expr, callback=callback) def _init_table(self, columns=None, value_names=None, data=None, sort=None): """Build the table.""" @@ -422,23 +453,25 @@ def _init_table(self, columns=None, value_names=None, data=None, sort=None): value_names_json = dumps(self.value_names) sort_json = dumps(sort) - b.body += ''' + b.body += f""" - ''' % (data_json, value_names_json, columns_json, sort_json) + """ self.build(lambda html: emit('ready', self)) - connect(event='select', sender=self, func=lambda *args: self.update(), last=True) + connect( + event='select', sender=self, func=lambda *args: self.update(), last=True + ) connect(event='ready', sender=self, func=lambda *args: self._set_ready()) def _set_ready(self): @@ -451,13 +484,13 @@ def is_ready(self): def sort_by(self, name, sort_dir='asc'): """Sort by a given variable.""" - logger.log(5, "Sort by `%s` %s.", name, sort_dir) - self.eval_js('table.sort_("{}", "{}");'.format(name, sort_dir)) + logger.log(5, 'Sort by `%s` %s.', name, sort_dir) + self.eval_js(f'table.sort_("{name}", "{sort_dir}");') def filter(self, text=''): """Filter the view with a Javascript expression.""" - logger.log(5, "Filter table with `%s`.", text) - self.eval_js('table.filter_("{}", true);'.format(text)) + logger.log(5, 'Filter table with `%s`.', text) + self.eval_js(f'table.filter_("{text}", true);') def get_ids(self, callback=None): """Get the list of ids.""" @@ -497,37 +530,37 @@ def select(self, ids, callback=None, **kwargs): """ ids = _uniq(ids) assert all(_is_integer(_) for _ in ids) - self.eval_js('table.select({}, {});'.format(dumps(ids), dumps(kwargs)), callback=callback) + self.eval_js(f'table.select({dumps(ids)}, {dumps(kwargs)});', callback=callback) def scroll_to(self, id): """Scroll until a given row is visible.""" - self.eval_js('table._scrollTo({});'.format(id)) + self.eval_js(f'table._scrollTo({id});') def set_busy(self, busy): """Set the busy state of the GUI.""" - self.eval_js('table.setBusy({});'.format('true' if busy else 'false')) + self.eval_js(f'table.setBusy({"true" if busy else "false"});') def get(self, id, callback=None): """Get the object given its id.""" - self.eval_js('table.get("id", {})[0]["_values"]'.format(id), callback=callback) + self.eval_js(f'table.get("id", {id})[0]["_values"]', callback=callback) def add(self, objects): """Add objects object to the table.""" if not objects: return - self.eval_js('table.add_({});'.format(dumps(objects))) + self.eval_js(f'table.add_({dumps(objects)});') def change(self, objects): """Change some objects.""" if not objects: return - self.eval_js('table.change_({});'.format(dumps(objects))) + self.eval_js(f'table.change_({dumps(objects)});') def remove(self, ids): """Remove some objects from their ids.""" if not ids: return - self.eval_js('table.remove_({});'.format(dumps(ids))) + self.eval_js(f'table.remove_({dumps(ids)});') def remove_all(self): """Remove all rows in the table.""" @@ -537,7 +570,7 @@ def remove_all_and_add(self, objects): """Remove all rows in the table and add new objects.""" if not objects: return self.remove_all() - self.eval_js('table.removeAllAndAdd({});'.format(dumps(objects))) + self.eval_js(f'table.removeAllAndAdd({dumps(objects)});') def get_selected(self, callback=None): """Get the currently selected rows.""" @@ -552,11 +585,13 @@ def get_current_sort(self, callback=None): # KeyValueWidget # ----------------------------------------------------------------------------- + class KeyValueWidget(QWidget): """A Qt widget that displays a simple form where each field has a name, a type, and accept user input.""" + def __init__(self, *args, **kwargs): - super(KeyValueWidget, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self._items = [] self._layout = QGridLayout(self) @@ -575,7 +610,7 @@ def add_pair(self, name, default=None, vtype=None): if isinstance(default, list): # Take lists into account. for i, value in enumerate(default): - self.add_pair('%s[%d]' % (name, i), default=value, vtype=vtype) + self.add_pair(f'{name}[{i}]', default=value, vtype=vtype) return if vtype is None and default is not None: vtype = type(default).__name__ @@ -601,7 +636,7 @@ def add_pair(self, name, default=None, vtype=None): widget = QCheckBox(self) widget.setChecked(default is True) else: # pragma: no cover - raise ValueError("Not supported vtype: %s." % vtype) + raise ValueError(f'Not supported vtype: {vtype}.') widget.setMaximumWidth(400) @@ -618,7 +653,8 @@ def add_pair(self, name, default=None, vtype=None): def names(self): """List of field names.""" return sorted( - set(i[0] if '[' not in i[0] else i[0][:i[0].index('[')] for i in self._items)) + {i[0] if '[' not in i[0] else i[0][: i[0].index('[')] for i in self._items} + ) def get_widget(self, name): """Get the widget of a field.""" @@ -629,15 +665,15 @@ def get_widget(self, name): def get_value(self, name): """Get the default or user-entered value of a field.""" # Detect if the requested name is a list type. - names = set(i[0] for i in self._items) - if '%s[0]' % name in names: + names = {i[0] for i in self._items} + if f'{name}[0]' in names: out = [] i = 0 - namei = '%s[%d]' % (name, i) + namei = f'{name}[{i}]' while namei in names: out.append(self.get_value(namei)) i += 1 - namei = '%s[%d]' % (name, i) + namei = f'{name}[{i}]' return out for name_, vtype, default, widget in self._items: if name_ == name: diff --git a/phy/plot/__init__.py b/phy/plot/__init__.py index 8ee863111..b0119781c 100644 --- a/phy/plot/__init__.py +++ b/phy/plot/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # flake8: noqa """Plotting module based on OpenGL. @@ -8,9 +7,9 @@ """ -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import os.path as op @@ -22,5 +21,13 @@ from .utils import get_linear_x, BatchAccumulator from .interact import Grid, Boxed, Lasso from .visuals import ( - ScatterVisual, UniformScatterVisual, PlotVisual, UniformPlotVisual, HistogramVisual, - TextVisual, LineVisual, ImageVisual, PolygonVisual) + ScatterVisual, + UniformScatterVisual, + PlotVisual, + UniformPlotVisual, + HistogramVisual, + TextVisual, + LineVisual, + ImageVisual, + PolygonVisual, +) diff --git a/phy/plot/axes.py b/phy/plot/axes.py index 34d18994d..e98ddcb15 100644 --- a/phy/plot/axes.py +++ b/phy/plot/axes.py @@ -1,28 +1,26 @@ -# -*- coding: utf-8 -*- - """Axes.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import numpy as np from matplotlib.ticker import MaxNLocator - - -from .transform import NDC, Range, _fix_coordinate_in_visual -from .visuals import LineVisual, TextVisual from phylib import connect from phylib.utils._types import _is_integer + from phy.gui.qt import is_high_dpi +from .transform import NDC, Range, _fix_coordinate_in_visual +from .visuals import LineVisual, TextVisual -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Utils -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ -class AxisLocator(object): + +class AxisLocator: """Determine the location of ticks in a view. Constructor @@ -85,9 +83,7 @@ def set_view_bounds(self, view_bounds=None): dy = 2 * (y1 - y0) # Get the bounds in data coordinates. - ((dx0, dy0), (dx1, dy1)) = self._tr.apply([ - [x0 - dx, y0 - dy], - [x1 + dx, y1 + dy]]) + ((dx0, dy0), (dx1, dy1)) = self._tr.apply([[x0 - dx, y0 - dy], [x1 + dx, y1 + dy]]) # Compute the ticks in data coordinates. self.xticks = self.locx.tick_values(dx0, dx1) @@ -102,9 +98,9 @@ def set_view_bounds(self, view_bounds=None): self.ytext = [fmt % v for v in self.yticks] -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Axes visual -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ def _set_line_data(xticks, yticks): @@ -125,10 +121,10 @@ def _quant_zoom(z): """Return the zoom level as a positive or negative integer.""" if z == 0: return 0 # pragma: no cover - return int(z) if z >= 1 else -int(1. / z) + return int(z) if z >= 1 else -int(1.0 / z) -class Axes(object): +class Axes: """Dynamic axes that move along the camera when panning and zooming. Constructor @@ -144,7 +140,8 @@ class Axes(object): Whether to show the horizontal grid lines. """ - default_color = (1, 1, 1, .25) + + default_color = (1, 1, 1, 0.25) def __init__(self, data_bounds=None, color=None, show_x=True, show_y=True): self.show_x = show_x @@ -223,8 +220,7 @@ def attach(self, canvas): # Exclude the axes visual from the interact/layout, but keep the PanZoom. interact = getattr(canvas, 'interact', None) exclude_origins = (interact,) if interact else () - canvas.add_visual( - visual, clearable=False, exclude_origins=exclude_origins) + canvas.add_visual(visual, clearable=False, exclude_origins=exclude_origins) self.locator.set_view_bounds(NDC) self.update_visuals() diff --git a/phy/plot/base.py b/phy/plot/base.py index e82f42002..75db4678b 100644 --- a/phy/plot/base.py +++ b/phy/plot/base.py @@ -1,44 +1,44 @@ -# -*- coding: utf-8 -*- - """Base OpenGL classes.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ -from contextlib import contextmanager import gc import logging import re +from contextlib import contextmanager, suppress from timeit import default_timer import numpy as np +from phylib.utils import Bunch, connect, emit + +from phy.gui.qt import QEvent, QOpenGLWindow, Qt -from phylib.utils import connect, emit, Bunch -from phy.gui.qt import Qt, QEvent, QOpenGLWindow from . import gloo from .gloo import gl -from .transform import TransformChain, Clip, pixels_to_ndc, Range -from .utils import _load_shader, _get_array, BatchAccumulator - +from .transform import Clip, Range, TransformChain, pixels_to_ndc +from .utils import BatchAccumulator, _get_array, _load_shader logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Utils -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def indent(text): - return '\n'.join(' ' + l.strip() for l in text.splitlines()) + return '\n'.join(f' {l.strip()}' for l in text.splitlines()) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Base spike visual -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + -class BaseVisual(object): +class BaseVisual: """A Visual represents one object (or homogeneous set of objects). It is rendered with a single pass of a single gloo program with a single type of GL primitive. @@ -94,9 +94,9 @@ def emit_visual_set_data(self): def set_shader(self, name): """Set the built-in vertex and fragment shader.""" - self.vertex_shader = _load_shader(name + '.vert') - self.fragment_shader = _load_shader(name + '.frag') - self.geometry_shader = _load_shader(name + '.geom') + self.vertex_shader = _load_shader(f'{name}.vert') + self.fragment_shader = _load_shader(f'{name}.frag') + self.geometry_shader = _load_shader(f'{name}.geom') def set_primitive_type(self, primitive_type): """Set the primitive type (points, lines, line_strip, line_fan, triangles).""" @@ -115,12 +115,14 @@ def on_draw(self): # Draw the program. self.program.draw(self.gl_primitive_type, self.index_buffer) else: # pragma: no cover - logger.debug("Skipping drawing visual `%s` because the program " - "has not been built yet.", self) + logger.debug( + 'Skipping drawing visual `%s` because the program has not been built yet.', + self, + ) def on_resize(self, width, height): """Update the window size in the OpenGL program.""" - s = self.program._vertex.code + '\n' + self.program.fragment.code + s = f'{self.program._vertex.code}\n{self.program.fragment.code}' # HACK: ensure that u_window_size appears somewhere in the shaders body (discarding # the headers). s = s.replace('uniform vec2 u_window_size;', '') @@ -177,8 +179,12 @@ def add_batch_data(self, **kwargs): # WARNING: size should be the number of items for correct batch array creation, # not the number of vertices. self._acc.add( - data, box_index=box_index, n_items=data._n_items, - n_vertices=data._n_vertices, noconcat=self._noconcat) + data, + box_index=box_index, + n_items=data._n_items, + n_vertices=data._n_vertices, + noconcat=self._noconcat, + ) def reset_batch(self): """Reinitialize the batch.""" @@ -201,27 +207,31 @@ def set_box_index(self, box_index, data=None): self.program['a_box_index'] = a_box_index.astype(np.float32) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Build program with layouts -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _get_glsl(to_insert, shader_type=None, location=None, exclude_origins=()): """From a `to_insert` list of (shader_type, location, origin, snippet), return the concatenated snippet that satisfies the specified shader type, location, and origin.""" - return '\n'.join(( + return '\n'.join( snippet for (shader_type_, location_, origin_, snippet) in to_insert - if shader_type_ == shader_type and location_ == location and - origin_ not in exclude_origins - )) + if shader_type_ == shader_type + and location_ == location + and origin_ not in exclude_origins + ) def _repl_vars(snippet, varout, varin): - snippet = snippet.replace('{{varout}}', varout if varout != 'gl_Position' else 'pos_tmp') + snippet = snippet.replace( + '{{varout}}', varout if varout != 'gl_Position' else 'pos_tmp' + ) return snippet.replace('{{varin}}', varin) -class GLSLInserter(object): +class GLSLInserter: """Object used to insert GLSL snippets into shader code. This class provides methods to specify the snippets to insert, and the @@ -237,7 +247,9 @@ def __init__(self): def _init_insert(self): self.insert_vert('vec2 {{varout}} = {{varin}};', 'before_transforms', index=0) - self.insert_vert('gl_Position = vec4({{varout}}, 0., 1.);', 'after_transforms', index=0) + self.insert_vert( + 'gl_Position = vec4({{varout}}, 0., 1.);', 'after_transforms', index=0 + ) self.insert_vert('varying vec2 v_{{varout}};\n', 'header', index=0) self.insert_frag('varying vec2 v_{{varout}};\n', 'header', index=0) @@ -289,9 +301,9 @@ def insert_frag(self, glsl, location=None, origin=None, index=None): def add_varying(self, vtype, name, value): """Add a varying variable.""" - self.insert_vert('varying %s %s;' % (vtype, name), 'header') - self.insert_frag('varying %s %s;' % (vtype, name), 'header') - self.insert_vert('%s = %s;' % (name, value), 'end') + self.insert_vert(f'varying {vtype} {name};', 'header') + self.insert_frag(f'varying {vtype} {name};', 'header') + self.insert_vert(f'{name} = {value};', 'end') def add_gpu_transforms(self, tc): """Insert all GLSL snippets from a transform chain.""" @@ -305,7 +317,9 @@ def add_gpu_transforms(self, tc): # Clipping. clip = tc.get('Clip') if clip: - self.insert_frag(clip.glsl('v_{{varout}}'), 'before_transforms', origin=origin) + self.insert_frag( + clip.glsl('v_{{varout}}'), 'before_transforms', origin=origin + ) def insert_into_shaders(self, vertex, fragment, exclude_origins=()): """Insert all GLSL snippets in a vertex and fragment shaders. @@ -345,28 +359,32 @@ def get_frag(t, loc): if not self._variables: logger.debug( "The vertex shader doesn't contain the transform placeholder: skipping the " - "transform chain GLSL insertion.") + 'transform chain GLSL insertion.' + ) return vertex, fragment assert self._variables # Define pos_orig only once. for varout, varin in self._variables: if varout == 'gl_Position': - self.insert_vert('vec2 pos_orig = %s;' % varin, 'before_transforms', index=0) + self.insert_vert( + f'vec2 pos_orig = {varin};', 'before_transforms', index=0 + ) # Replace the variable placeholders. to_insert = [] - for (shader_type, location, origin, glsl) in self._to_insert: + for shader_type, location, origin, glsl in self._to_insert: if '{{varout}}' not in glsl: to_insert.append((shader_type, location, origin, glsl)) else: for varout, varin in self._variables: to_insert.append( - (shader_type, location, origin, _repl_vars(glsl, varout, varin))) + (shader_type, location, origin, _repl_vars(glsl, varout, varin)) + ) # Headers. - vertex = get_vert(to_insert, 'header') + '\n\n' + vertex - fragment = get_frag(to_insert, 'header') + '\n\n' + fragment + vertex = f'{get_vert(to_insert, "header")}\n\n{vertex}' + fragment = f'{get_frag(to_insert, "header")}\n\n{fragment}' # Get the pre and post transforms. vs_insert = get_vert(self._to_insert, 'before_transforms') @@ -377,7 +395,12 @@ def get_frag(t, loc): def repl(m): varout, varin = m.group(1), m.group(2) varout = varout if varout != 'gl_Position' else 'pos_tmp' - return indent(vs_insert).replace('{{varout}}', varout).replace('{{varin}}', varin) + return ( + indent(vs_insert) + .replace('{{varout}}', varout) + .replace('{{varin}}', varin) + ) + vertex = self._transform_regex.sub(repl, vertex) # Insert snippets at the very start of the vertex shader. @@ -386,11 +409,11 @@ def repl(m): # Insert snippets at the very end of the vertex shader. i = vertex.rindex('}') - vertex = vertex[:i] + get_vert(to_insert, 'end') + '}\n' + vertex = f'{vertex[:i] + get_vert(to_insert, "end")}}}\n' # Insert snippets at the very end of the fragment shader. i = fragment.rindex('}') - fragment = fragment[:i] + get_frag(to_insert, 'end') + '}\n' + fragment = f'{fragment[:i] + get_frag(to_insert, "end")}}}\n' # Now, we make the replacements in the fragment shader. fs_insert = r'\1\n' + get_frag(to_insert, 'before_transforms') @@ -404,22 +427,22 @@ def __add__(self, inserter): return self -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Base canvas -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def get_modifiers(e): """Return modifier names from a Qt event.""" m = e.modifiers() return tuple( - name for name in ('Shift', 'Control', 'Alt', 'Meta') if m & getattr(Qt, name + 'Modifier')) + name + for name in ('Shift', 'Control', 'Alt', 'Meta') + if m & getattr(Qt, f'{name}Modifier') + ) -_BUTTON_MAP = { - 1: 'Left', - 2: 'Right', - 4: 'Middle' -} +_BUTTON_MAP = {1: 'Left', 2: 'Right', 4: 'Middle'} _SUPPORTED_KEYS = ( @@ -464,7 +487,7 @@ def mouse_info(e): p = e.pos() x, y = p.x(), p.y() b = e.button() - return (x, y), _BUTTON_MAP.get(b, None) + return (x, y), _BUTTON_MAP.get(b) def key_info(e): @@ -474,7 +497,7 @@ def key_info(e): return chr(key) else: for name in _SUPPORTED_KEYS: - if key == getattr(Qt, 'Key_' + name, None): + if key == getattr(Qt, f'Key_{name}', None): return name @@ -487,21 +510,22 @@ class LazyProgram(gloo.Program): should always be sent from the main GUI thread. """ + def __init__(self, *args, **kwargs): self._update_queue = [] self._is_lazy = False - super(LazyProgram, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def __setitem__(self, name, data): # Remove all past items with the current name. if self._is_lazy: - self._update_queue[:] = ((n, d) for (n, d) in self._update_queue if n != name) + self._update_queue[:] = ( + (n, d) for (n, d) in self._update_queue if n != name + ) self._update_queue.append((name, data)) else: - try: - super(LazyProgram, self).__setitem__(name, data) - except IndexError: - pass + with suppress(IndexError): + super().__setitem__(name, data) class BaseCanvas(QOpenGLWindow): @@ -513,7 +537,7 @@ class BaseCanvas(QOpenGLWindow): """ def __init__(self, *args, **kwargs): - super(BaseCanvas, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.gpu_transforms = TransformChain() self.inserter = GLSLInserter() self.visuals = [] @@ -527,7 +551,7 @@ def __init__(self, *args, **kwargs): self._mouse_press_button = None self._mouse_press_modifiers = None self._last_mouse_pos = None - self._mouse_press_time = 0. + self._mouse_press_time = 0.0 self._current_key_event = None # Default window size. @@ -542,8 +566,10 @@ def window_to_ndc(self, mouse_pos): account pan and zoom.""" panzoom = getattr(self, 'panzoom', None) ndc = ( - panzoom.window_to_ndc(mouse_pos) if panzoom else - np.asarray(pixels_to_ndc(mouse_pos, size=self.get_size()))) + panzoom.window_to_ndc(mouse_pos) + if panzoom + else np.asarray(pixels_to_ndc(mouse_pos, size=self.get_size())) + ) return ndc # Queue @@ -576,7 +602,7 @@ def remove(self, *visuals): visuals = [v for v in visuals if v is not None] self.visuals[:] = (v for v in self.visuals if v.visual not in visuals) for v in visuals: - logger.log(5, "Remove visual %s.", v) + logger.log(5, 'Remove visual %s.', v) v.close() del v gc.collect() @@ -606,7 +632,7 @@ def add_visual(self, visual, **kwargs): """ if self.has_visual(visual): - logger.log(5, "This visual has already been added.") + logger.log(5, 'This visual has already been added.') return visual.canvas = self # This is the list of origins (mostly, interacts and layouts) that should be ignored @@ -631,12 +657,13 @@ def add_visual(self, visual, **kwargs): gs = getattr(visual, 'geometry_shader', None) if gs: gs = gloo.GeometryShader( - gs, visual.geometry_count, visual.geometry_in, visual.geometry_out) + gs, visual.geometry_count, visual.geometry_in, visual.geometry_out + ) # Finally, we create the visual's program. visual.program = LazyProgram(vs, fs, gs) - logger.log(5, "Vertex shader: %s", vs) - logger.log(5, "Fragment shader: %s", fs) + logger.log(5, 'Vertex shader: %s', vs) + logger.log(5, 'Fragment shader: %s', fs) # Initialize the size. visual.on_resize(self.size().width(), self.size().height()) @@ -647,10 +674,7 @@ def add_visual(self, visual, **kwargs): def has_visual(self, visual): """Return whether a visual belongs to the canvas.""" - for v in self.visuals: - if v.visual == visual: - return True - return False + return any(v.visual == visual for v in self.visuals) def iter_update_queue(self): """Iterate through all OpenGL program updates called in lazy mode.""" @@ -672,7 +696,7 @@ def initializeGL(self): try: gl.enable_depth_mask() except Exception as e: # pragma: no cover - logger.debug("Exception in initializetGL: %s", str(e)) + logger.debug('Exception in initializetGL: %s', str(e)) return def paintGL(self): @@ -687,19 +711,24 @@ def paintGL(self): # Draw all visuals, clearable first, non clearable last. visuals = [v for v in self.visuals if v.get('clearable', True)] visuals += [v for v in self.visuals if not v.get('clearable', True)] - logger.log(5, "Draw %d visuals.", len(visuals)) + logger.log(5, 'Draw %d visuals.', len(visuals)) for v in visuals: visual = v.visual if size != self._size: visual.on_resize(*size) # Do not draw if there are no vertices. - if not visual._hidden and visual.n_vertices > 0 and size[0] > 10 and size[1] > 10: - logger.log(5, "Draw visual `%s`.", visual) + if ( + not visual._hidden + and visual.n_vertices > 0 + and size[0] > 10 + and size[1] > 10 + ): + logger.log(5, 'Draw visual `%s`.', visual) visual.on_draw() self._size = size except Exception as e: # pragma: no cover # raise e - logger.debug("Exception in paintGL: %s", str(e)) + logger.debug('Exception in paintGL: %s', str(e)) return # Events @@ -713,7 +742,7 @@ def attach_events(self, obj): def emit(self, name, **kwargs): """Raise an internal event and call `on_xxx()` on attached objects.""" for obj in self._attached: - f = getattr(obj, 'on_' + name, None) + f = getattr(obj, f'on_{name}', None) if f: f(Bunch(kwargs)) @@ -744,7 +773,7 @@ def mouseReleaseEvent(self, e): """Emit an internal `mouse_release` or `mouse_click` event.""" self._mouse_event('mouse_release', e) # HACK: since there is no mouseClickEvent in Qt, emulate it here. - if default_timer() - self._mouse_press_time < .25: + if default_timer() - self._mouse_press_time < 0.25: self._mouse_event('mouse_click', e) self._mouse_press_position = None self._mouse_press_button = None @@ -765,7 +794,8 @@ def mouseMoveEvent(self, e): modifiers=modifiers, mouse_press_modifiers=self._mouse_press_modifiers, button=self._mouse_press_button, - mouse_press_position=self._mouse_press_position) + mouse_press_position=self._mouse_press_position, + ) self._last_mouse_pos = pos def wheelEvent(self, e): # pragma: no cover @@ -796,14 +826,14 @@ def keyReleaseEvent(self, e): def event(self, e): # pragma: no cover """Touch event.""" - out = super(BaseCanvas, self).event(e) + out = super().event(e) t = e.type() # Two-finger pinch. - if (t == QEvent.TouchBegin): + if t == QEvent.TouchBegin: self.emit('pinch_begin') - elif (t == QEvent.TouchEnd): + elif t == QEvent.TouchEnd: self.emit('pinch_end') - elif (t == QEvent.Gesture): + elif t == QEvent.Gesture: gesture = e.gesture(Qt.PinchGesture) if gesture: (x, y) = gesture.centerPoint().x(), gesture.centerPoint().y() @@ -811,30 +841,39 @@ def event(self, e): # pragma: no cover last_scale = gesture.lastScaleFactor() rotation = gesture.rotationAngle() self.emit( - 'pinch', pos=(x, y), - scale=scale, last_scale=last_scale, rotation=rotation) + 'pinch', + pos=(x, y), + scale=scale, + last_scale=last_scale, + rotation=rotation, + ) # General touch event. - elif (t == QEvent.TouchUpdate): + elif t == QEvent.TouchUpdate: points = e.touchPoints() # These variables are lists of (x, y) coordinates. - pos, last_pos = zip(*[ - ((p.pos().x(), p.pos.y()), (p.lastPos().x(), p.lastPos.y())) - for p in points]) + pos, last_pos = zip( + *[ + ((p.pos().x(), p.pos.y()), (p.lastPos().x(), p.lastPos.y())) + for p in points + ] + ) self.emit('touch', pos=pos, last_pos=last_pos) return out def update(self): """Update the OpenGL canvas.""" if not self._is_lazy: - super(BaseCanvas, self).update() + super().update() -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Base layout -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ -class BaseLayout(object): + +class BaseLayout: """Implement global transforms on a canvas, like subplots.""" + canvas = None box_var = None n_dims = 1 @@ -897,13 +936,18 @@ def box_map(self, mouse_pos): def update_visual(self, visual): """Called whenever visual.set_data() is called. Set a_box_index in here.""" - if (visual.n_vertices > 0 and - self.box_var in visual.program and - ((visual.program[self.box_var] is None) or - (visual.program[self.box_var].shape[0] != visual.n_vertices))): - logger.log(5, "Set %s(%d) for %s" % (self.box_var, visual.n_vertices, visual)) + if ( + visual.n_vertices > 0 + and self.box_var in visual.program + and ( + (visual.program[self.box_var] is None) + or (visual.program[self.box_var].shape[0] != visual.n_vertices) + ) + ): + logger.log(5, f'Set {self.box_var}({visual.n_vertices}) for {visual}') visual.program[self.box_var] = _get_array( - self.active_box, (visual.n_vertices, self.n_dims)).astype(np.float32) + self.active_box, (visual.n_vertices, self.n_dims) + ).astype(np.float32) def update(self): """Update all visuals in the attached canvas.""" diff --git a/phy/plot/gloo/array.py b/phy/plot/gloo/array.py index 5e5c22ddf..cfaaf3299 100644 --- a/phy/plot/gloo/array.py +++ b/phy/plot/gloo/array.py @@ -23,10 +23,9 @@ import numpy as np from . import gl -from .gpudata import GPUData -from .globject import GLObject from .buffer import VertexBuffer - +from .globject import GLObject +from .gpudata import GPUData log = logging.getLogger(__name__) @@ -46,40 +45,40 @@ def __init__(self, usage=gl.GL_DYNAMIC_DRAW): @property def need_update(self): - """ Whether object needs to be updated """ + """Whether object needs to be updated""" return self._buffer.need_update def _update(self): - """ Upload all pending data to GPU. """ + """Upload all pending data to GPU.""" self._buffer._update() def _create(self): - """ Create vertex array on GPU """ + """Create vertex array on GPU""" self._handle = gl.glGenVertexArrays(1) - log.debug("GPU: Creating vertex array (id=%d)" % self._id) + log.debug(f'GPU: Creating vertex array (id={self._id})') self._deactivate() self._buffer._create() def _delete(self): - """ Delete vertex array from GPU """ + """Delete vertex array from GPU""" if self._handle > -1: self._buffer._delete() gl.glDeleteVertexArrays(1, np.array([self._handle])) def _activate(self): - """ Bind the array """ + """Bind the array""" - log.debug("GPU: Activating array (id=%d)" % self._id) + log.debug(f'GPU: Activating array (id={self._id})') gl.glBindVertexArray(self._handle) self._buffer._activate() def _deactivate(self): - """ Unbind the current bound array """ + """Unbind the current bound array""" self._buffer._deactivate() - log.debug("GPU: Deactivating array (id=%d)" % self._id) + log.debug(f'GPU: Deactivating array (id={self._id})') gl.glBindVertexArray(0) diff --git a/phy/plot/gloo/atlas.py b/phy/plot/gloo/atlas.py index f3b0933c3..14033c6c3 100644 --- a/phy/plot/gloo/atlas.py +++ b/phy/plot/gloo/atlas.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Copyright (c) 2009-2016 Nicolas P. Rougier. All rights reserved. # Distributed under the (new) BSD License. @@ -19,13 +18,14 @@ import logging import sys -from . texture import Texture2D + +from .texture import Texture2D log = logging.getLogger(__name__) class Atlas(Texture2D): - """ Texture Atlas (two dimensional) + """Texture Atlas (two dimensional) Parameters @@ -53,7 +53,9 @@ class Atlas(Texture2D): def __init__(self): Texture2D.__init__(self) - self.nodes = [(0, 0, self.width), ] + self.nodes = [ + (0, 0, self.width), + ] self.used = 0 def allocate(self, shape): @@ -82,15 +84,16 @@ def allocate(self, shape): y = self._fit(i, width, height) if y >= 0: node = self.nodes[i] - if (y + height < best_height or - (y + height == best_height and node[2] < best_width)): + if y + height < best_height or ( + y + height == best_height and node[2] < best_width + ): best_height = y + height best_index = i best_width = node[2] region = node[0], y, width, height if best_index == -1: - log.warning("No enough free space in atlas") + log.warning('No enough free space in atlas') return None node = region[0], region[1] + height, width @@ -155,7 +158,7 @@ def _fit(self, index, width, height): return y def _merge(self): - """ Merge nodes. """ + """Merge nodes.""" i = 0 while i < len(self.nodes) - 1: diff --git a/phy/plot/gloo/buffer.py b/phy/plot/gloo/buffer.py index 27cc00e80..4a5c434ec 100644 --- a/phy/plot/gloo/buffer.py +++ b/phy/plot/gloo/buffer.py @@ -26,9 +26,8 @@ import numpy as np from . import gl -from .gpudata import GPUData from .globject import GLObject - +from .gpudata import GPUData log = logging.getLogger(__name__) @@ -48,59 +47,59 @@ def __init__(self, target, usage=gl.GL_DYNAMIC_DRAW): @property def need_update(self): - """ Whether object needs to be updated """ + """Whether object needs to be updated""" return self.pending_data is not None def _create(self): - """ Create buffer on GPU """ + """Create buffer on GPU""" self._handle = gl.glGenBuffers(1) self._activate() - log.log(5, "GPU: Creating buffer (id=%d)" % self._id) + log.log(5, f'GPU: Creating buffer (id={self._id})') gl.glBufferData(self._target, self.nbytes, None, self._usage) self._deactivate() def _delete(self): - """ Delete buffer from GPU """ + """Delete buffer from GPU""" if self._handle > -1: gl.glDeleteBuffers(1, np.array([self._handle])) def _activate(self): - """ Bind the buffer to some target """ + """Bind the buffer to some target""" - log.log(5, "GPU: Activating buffer (id=%d)" % self._id) + log.log(5, f'GPU: Activating buffer (id={self._id})') gl.glBindBuffer(self._target, self._handle) def _deactivate(self): - """ Unbind the current bound buffer """ + """Unbind the current bound buffer""" - log.log(5, "GPU: Deactivating buffer (id=%d)" % self._id) + log.log(5, f'GPU: Deactivating buffer (id={self._id})') gl.glBindBuffer(self._target, 0) def _update(self): - """ Upload all pending data to GPU. """ + """Upload all pending data to GPU.""" if self.pending_data: start, stop = self.pending_data offset, nbytes = start, stop - start # offset, nbytes = self.pending_data - data = self.ravel().view(np.ubyte)[offset:offset + nbytes] + data = self.ravel().view(np.ubyte)[offset : offset + nbytes] gl.glBufferSubData(self.target, offset, nbytes, data) self._pending_data = None self._need_update = False class VertexBuffer(Buffer): - """ Buffer for vertex attribute data """ + """Buffer for vertex attribute data""" def __init__(self, usage=gl.GL_DYNAMIC_DRAW): Buffer.__init__(self, gl.GL_ARRAY_BUFFER, usage) class IndexBuffer(Buffer): - """ Buffer for index data """ + """Buffer for index data""" def __init__(self, usage=gl.GL_DYNAMIC_DRAW): Buffer.__init__(self, gl.GL_ELEMENT_ARRAY_BUFFER, usage) diff --git a/phy/plot/gloo/framebuffer.py b/phy/plot/gloo/framebuffer.py index 2e561aab2..ebc187f02 100644 --- a/phy/plot/gloo/framebuffer.py +++ b/phy/plot/gloo/framebuffer.py @@ -39,12 +39,11 @@ def on_draw(dt): from .globject import GLObject from .texture import Texture2D - log = logging.getLogger(__name__) class RenderBuffer(GLObject): - """ Base class for render buffer object. + """Base class for render buffer object. :param GLEnum format: Buffer format :param int width: Buffer width (pixels) @@ -61,17 +60,17 @@ def __init__(self, width=0, height=0, format=None): @property def width(self): - """ Buffer width (read-only). """ + """Buffer width (read-only).""" return self._width @property def height(self): - """ Buffer height (read-only). """ + """Buffer height (read-only).""" return self._height def resize(self, width, height): - """ Resize the buffer (deferred operation). + """Resize the buffer (deferred operation). :param int width: New buffer width (pixels) :param int height: New buffer height (pixels) @@ -83,44 +82,43 @@ def resize(self, width, height): self._height = height def _create(self): - """ Create buffer on GPU """ + """Create buffer on GPU""" - log.debug("GPU: Create render buffer") + log.debug('GPU: Create render buffer') self._handle = gl.glGenRenderbuffers(1) def _delete(self): - """ Delete buffer from GPU """ + """Delete buffer from GPU""" - log.debug("GPU: Deleting render buffer") + log.debug('GPU: Deleting render buffer') gl.glDeleteRenderbuffer(self._handle) def _activate(self): - """ Activate buffer on GPU """ + """Activate buffer on GPU""" - log.debug("GPU: Activate render buffer") + log.debug('GPU: Activate render buffer') gl.glBindRenderbuffer(gl.GL_RENDERBUFFER, self._handle) if self._need_resize: self._resize() self._need_resize = False def _deactivate(self): - """ Deactivate buffer on GPU """ + """Deactivate buffer on GPU""" - log.debug("GPU: Deactivate render buffer") + log.debug('GPU: Deactivate render buffer') gl.glBindRenderbuffer(gl.GL_RENDERBUFFER, 0) def _resize(self): - """ Buffer resize on GPU """ + """Buffer resize on GPU""" # WARNING: width/height should be checked against maximum size # maxsize = gl.glGetParameter(gl.GL_MAX_RENDERBUFFER_SIZE) - log.debug("GPU: Resize render buffer") - gl.glRenderbufferStorage(self._target, self._format, - self._width, self._height) + log.debug('GPU: Resize render buffer') + gl.glRenderbufferStorage(self._target, self._format, self._width, self._height) class ColorBuffer(RenderBuffer): - """ Color buffer object. + """Color buffer object. :param int width: Buffer width (pixels) :param int height: Buffer height (pixel) @@ -134,7 +132,7 @@ def __init__(self, width, height, format=gl.GL_RGBA): class DepthBuffer(RenderBuffer): - """ Depth buffer object. + """Depth buffer object. :param int width: Buffer width (pixels) :param int height: Buffer height (pixel) @@ -148,7 +146,7 @@ def __init__(self, width, height, format=gl.GL_DEPTH_COMPONENT): class StencilBuffer(RenderBuffer): - """ Stencil buffer object + """Stencil buffer object :param int width: Buffer width (pixels) :param int height: Buffer height (pixel) @@ -162,7 +160,7 @@ def __init__(self, width, height, format=gl.GL_STENCIL_INDEX8): class FrameBuffer(GLObject): - """ Framebuffer object. + """Framebuffer object. :param ColorBuffer color: One or several color buffers or None :param DepthBuffer depth: A depth buffer or None @@ -170,8 +168,7 @@ class FrameBuffer(GLObject): """ def __init__(self, color=None, depth=None, stencil=None): - """ - """ + """ """ GLObject.__init__(self) @@ -192,13 +189,13 @@ def __init__(self, color=None, depth=None, stencil=None): @property def color(self): - """ Color buffer attachment(s) (read/write) """ + """Color buffer attachment(s) (read/write)""" return self._color @color.setter def color(self, buffers): - """ Color buffer attachment(s) (read/write) """ + """Color buffer attachment(s) (read/write)""" if not isinstance(buffers, list): buffers = [buffers] @@ -207,9 +204,9 @@ def color(self, buffers): for i, buffer in enumerate(buffers): if self.width != 0 and self.width != buffer.width: - raise ValueError("Buffer width does not match") + raise ValueError('Buffer width does not match') elif self.height != 0 and self.height != buffer.height: - raise ValueError("Buffer height does not match") + raise ValueError('Buffer height does not match') self._width = buffer.width self._height = buffer.height @@ -219,24 +216,23 @@ def color(self, buffers): if isinstance(buffer, (ColorBuffer, Texture2D)) or buffer is None: self._pending_attachments.append((target, buffer)) else: - raise ValueError( - "Buffer must be a ColorBuffer, Texture2D or None") + raise ValueError('Buffer must be a ColorBuffer, Texture2D or None') self._need_attach = True @property def depth(self): - """ Depth buffer attachment (read/write) """ + """Depth buffer attachment (read/write)""" return self._depth @depth.setter def depth(self, buffer): - """ Depth buffer attachment (read/write) """ + """Depth buffer attachment (read/write)""" if self.width != 0 and self.width != buffer.width: - raise ValueError("Buffer width does not match") + raise ValueError('Buffer width does not match') elif self.height != 0 and self.height != buffer.height: - raise ValueError("Buffer height does not match") + raise ValueError('Buffer height does not match') self._width = buffer.width self._height = buffer.height @@ -245,24 +241,23 @@ def depth(self, buffer): if isinstance(buffer, (DepthBuffer, Texture2D)) or buffer is None: self._pending_attachments.append((target, buffer)) else: - raise ValueError( - "Buffer must be a DepthBuffer, Texture2D or None") + raise ValueError('Buffer must be a DepthBuffer, Texture2D or None') self._need_attach = True @property def stencil(self): - """ Stencil buffer attachment (read/write) """ + """Stencil buffer attachment (read/write)""" return self._stencil @stencil.setter def stencil(self, buffer): - """ Stencil buffer attachment (read/write) """ + """Stencil buffer attachment (read/write)""" if self.width != 0 and self.width != buffer.width: - raise ValueError("Buffer width does not match") + raise ValueError('Buffer width does not match') elif self.height != 0 and self.height != buffer.height: - raise ValueError("Buffer height does not match") + raise ValueError('Buffer height does not match') self._width = buffer.width self._height = buffer.height @@ -271,24 +266,23 @@ def stencil(self, buffer): if isinstance(buffer, StencilBuffer) or buffer is None: self._pending_attachments.append((target, buffer)) else: - raise ValueError( - "Buffer must be a StencilBuffer, Texture2D or None") + raise ValueError('Buffer must be a StencilBuffer, Texture2D or None') self._need_attach = True @property def width(self): - """ Buffer width (read only, pixels) """ + """Buffer width (read only, pixels)""" return self._width @property def height(self): - """ Buffer height (read only, pixels) """ + """Buffer height (read only, pixels)""" return self._height def resize(self, width, height): - """ Resize the buffer (deferred operation). + """Resize the buffer (deferred operation). This method will also resize any attached buffers. @@ -327,8 +321,7 @@ def resize(self, width, height): if isinstance(self.stencil, StencilBuffer): self.stencil.resize(width, height) elif isinstance(self.stencil, Texture2D): - stencil = np.resize( - self.stencil, (height, width, self.stencil.shape[2])) + stencil = np.resize(self.stencil, (height, width, self.stencil.shape[2])) stencil = stencil.view(self.stencil.__class__) self.stencil.delete() self.stencil = stencil @@ -338,59 +331,59 @@ def resize(self, width, height): self._need_attach = True def _create(self): - """ Create framebuffer on GPU """ + """Create framebuffer on GPU""" - log.debug("GPU: Create framebuffer") + log.debug('GPU: Create framebuffer') self._handle = gl.glGenFramebuffers(1) def _delete(self): - """ Delete buffer from GPU """ + """Delete buffer from GPU""" - log.debug("GPU: Delete framebuffer") + log.debug('GPU: Delete framebuffer') gl.glDeleteFramebuffer(self._handle) def _activate(self): - """ Activate framebuffer on GPU """ + """Activate framebuffer on GPU""" - log.debug("GPU: Activate render framebuffer") + log.debug('GPU: Activate render framebuffer') gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self._handle) if self._need_attach: self._attach() self._need_attach = False - attachments = [gl.GL_COLOR_ATTACHMENT0 + - i for i in range(len(self.color))] + attachments = [gl.GL_COLOR_ATTACHMENT0 + i for i in range(len(self.color))] gl.glDrawBuffers(np.array(attachments, dtype=np.uint32)) def _deactivate(self): - """ Deactivate framebuffer on GPU """ + """Deactivate framebuffer on GPU""" - log.debug("GPU: Deactivate render framebuffer") + log.debug('GPU: Deactivate render framebuffer') gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0) # gl.glDrawBuffers([gl.GL_COLOR_ATTACHMENT0]) def _attach(self): - """ Attach render buffers to framebuffer """ + """Attach render buffers to framebuffer""" - log.debug("GPU: Attach render buffers") + log.debug('GPU: Attach render buffers') while self._pending_attachments: attachment, buffer = self._pending_attachments.pop(0) if buffer is None: - gl.glFramebufferRenderbuffer(gl.GL_FRAMEBUFFER, attachment, - gl.GL_RENDERBUFFER, 0) + gl.glFramebufferRenderbuffer(gl.GL_FRAMEBUFFER, attachment, gl.GL_RENDERBUFFER, 0) elif isinstance(buffer, RenderBuffer): buffer.activate() - gl.glFramebufferRenderbuffer(gl.GL_FRAMEBUFFER, attachment, - gl.GL_RENDERBUFFER, buffer.handle) + gl.glFramebufferRenderbuffer( + gl.GL_FRAMEBUFFER, attachment, gl.GL_RENDERBUFFER, buffer.handle + ) buffer.deactivate() elif isinstance(buffer, Texture2D): buffer.activate() # INFO: 0 is for mipmap level 0 (default) of the texture - gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, attachment, - buffer.target, buffer.handle, 0) + gl.glFramebufferTexture2D( + gl.GL_FRAMEBUFFER, attachment, buffer.target, buffer.handle, 0 + ) buffer.deactivate() else: - raise ValueError("Invalid attachment") + raise ValueError('Invalid attachment') res = gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER) if res == gl.GL_FRAMEBUFFER_COMPLETE: @@ -398,17 +391,14 @@ def _attach(self): elif res == 0: raise RuntimeError('Target not equal to GL_FRAMEBUFFER') elif res == gl.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: - raise RuntimeError( - 'FrameBuffer attachments are incomplete.') + raise RuntimeError('FrameBuffer attachments are incomplete.') elif res == gl.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: - raise RuntimeError( - 'No valid attachments in the FrameBuffer.') + raise RuntimeError('No valid attachments in the FrameBuffer.') elif res == gl.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS: - raise RuntimeError( - 'attachments do not have the same width and height.') + raise RuntimeError('attachments do not have the same width and height.') elif res == gl.GL_FRAMEBUFFER_INCOMPLETE_FORMATS: - raise RuntimeError('Internal format of attachment ' - 'is not renderable.') + raise RuntimeError('Internal format of attachment is not renderable.') elif res == gl.GL_FRAMEBUFFER_UNSUPPORTED: - raise RuntimeError('Combination of internal formats used ' - 'by attachments is not supported.') + raise RuntimeError( + 'Combination of internal formats used by attachments is not supported.' + ) diff --git a/phy/plot/gloo/gl.py b/phy/plot/gloo/gl.py index 48c440fe0..cf80d2006 100644 --- a/phy/plot/gloo/gl.py +++ b/phy/plot/gloo/gl.py @@ -12,30 +12,38 @@ import ctypes import OpenGL + OpenGL.ERROR_ON_COPY = True # -> if set to a True value before importing the numpy/lists support modules, # will cause array operations to raise OpenGL.error.CopyError if the # operation would cause a data-copy in order to make the passed data-type # match the target data-type. -FormatHandler('gloo', - 'OpenGL.arrays.numpymodule.NumpyHandler', [ - 'gloo.buffer.VertexBuffer', - 'gloo.buffer.IndexBuffer', - 'gloo.atlas.Atlas', - 'gloo.texture.Texture2D', - 'gloo.texture.Texture1D', - 'gloo.texture.FloatTexture2D', - 'gloo.texture.FloatTexture1D', - 'gloo.texture.TextureCube', - ]) +FormatHandler( + 'gloo', + 'OpenGL.arrays.numpymodule.NumpyHandler', + [ + 'gloo.buffer.VertexBuffer', + 'gloo.buffer.IndexBuffer', + 'gloo.atlas.Atlas', + 'gloo.texture.Texture2D', + 'gloo.texture.Texture1D', + 'gloo.texture.FloatTexture2D', + 'gloo.texture.FloatTexture1D', + 'gloo.texture.TextureCube', + ], +) def cleanupCallback(context=None): """Create a cleanup callback to clear context-specific storage for the current context""" - def callback(context=contextdata.getContext(context)): + + def callback(context=None): # ← Remove the function call from default """Clean up the context, assumes that the context will *not* render again!""" + if context is None: # ← Handle None case inside the function + context = contextdata.getContext(context) contextdata.cleanupContext(context) + return callback @@ -46,10 +54,10 @@ def clear(color=(0, 0, 0, 10)): def enable_depth_mask(): glClearColor(0, 0, 0, 0) # noqa - glClearDepth(1.) # noqa + glClearDepth(1.0) # noqa glEnable(GL_BLEND) # noqa - glDepthRange(0., 1.) # noqa + glDepthRange(0.0, 1.0) # noqa glDepthFunc(GL_EQUAL) # noqa glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) # noqa @@ -69,9 +77,15 @@ def glGetActiveAttrib(program, index): type = ctypes.c_int() name = ctypes.create_string_buffer(bufsize) # Call - _glGetActiveAttrib(program, index, - bufsize, ctypes.byref(length), ctypes.byref(size), - ctypes.byref(type), name) + _glGetActiveAttrib( + program, + index, + bufsize, + ctypes.byref(length), + ctypes.byref(size), + ctypes.byref(type), + name, + ) # Return Python objects return name.value, size.value, type.value diff --git a/phy/plot/gloo/globject.py b/phy/plot/gloo/globject.py index fdddc80b5..1d2c1d74a 100644 --- a/phy/plot/gloo/globject.py +++ b/phy/plot/gloo/globject.py @@ -8,14 +8,14 @@ log = logging.getLogger(__name__) -class GLObject(object): - """ Generic GL object that may live both on CPU and GPU """ +class GLObject: + """Generic GL object that may live both on CPU and GPU""" # Internal id counter to keep track of GPU objects _idcount = 0 def __init__(self): - """ Initialize the object in the default state """ + """Initialize the object in the default state""" self._handle = -1 self._target = None @@ -44,26 +44,26 @@ def __init__(self): @property def need_create(self): - """ Whether object needs to be created """ + """Whether object needs to be created""" return self._need_create @property def need_update(self): - """ Whether object needs to be updated """ + """Whether object needs to be updated""" return self._need_update @property def need_setup(self): - """ Whether object needs to be setup """ + """Whether object needs to be setup""" return self._need_setup @property def need_delete(self): - """ Whether object needs to be deleted """ + """Whether object needs to be deleted""" return self._need_delete def delete(self): - """ Delete the object from GPU memory """ + """Delete the object from GPU memory""" # if self.need_delete: self._delete() @@ -74,9 +74,9 @@ def delete(self): self._need_delete = False def activate(self): - """ Activate the object on GPU """ + """Activate the object on GPU""" - if hasattr(self, "base") and isinstance(self.base, GLObject): + if hasattr(self, 'base') and isinstance(self.base, GLObject): self.base.activate() return @@ -91,63 +91,51 @@ def activate(self): self._need_setup = False if self.need_update: - log.log(5, "%s need update" % self.handle) + log.log(5, f'{self.handle} need update') self._update() self._need_update = False def deactivate(self): - """ Deactivate the object on GPU """ + """Deactivate the object on GPU""" - if hasattr(self, "base") and isinstance(self.base, GLObject): + if hasattr(self, 'base') and isinstance(self.base, GLObject): self.base.deactivate() else: self._deactivate() @property def handle(self): - """ Name of this object on the GPU """ + """Name of this object on the GPU""" - if hasattr(self, "base") and isinstance(self.base, GLObject): - if hasattr(self.base, "_handle"): + if hasattr(self, 'base') and isinstance(self.base, GLObject): + if hasattr(self.base, '_handle'): return self.base._handle return self._handle # return self._handle @property def target(self): - """ OpenGL type of object. """ + """OpenGL type of object.""" - if hasattr(self, "base") and isinstance(self.base, GLObject): + if hasattr(self, 'base') and isinstance(self.base, GLObject): return self.base._target return self._target # return self._handle def _create(self): - """ Dummy create method """ - - pass + """Dummy create method""" def _delete(self): - """ Dummy delete method """ - - pass + """Dummy delete method""" def _activate(self): - """ Dummy activate method """ - - pass + """Dummy activate method""" def _deactivate(self): - """ Dummy deactivate method """ - - pass + """Dummy deactivate method""" def _setup(self): - """ Dummy setup method """ - - pass + """Dummy setup method""" def _update(self): - """ Dummy update method """ - - pass + """Dummy update method""" diff --git a/phy/plot/gloo/gpudata.py b/phy/plot/gloo/gpudata.py index 8e0dd9591..21d7026ad 100644 --- a/phy/plot/gloo/gpudata.py +++ b/phy/plot/gloo/gpudata.py @@ -47,7 +47,7 @@ def __array_finalize__(self, obj): @property def pending_data(self): - """ Pending data region as (byte offset, byte size) """ + """Pending data region as (byte offset, byte size)""" if isinstance(self.base, GPUData): return self.base.pending_data @@ -59,7 +59,7 @@ def pending_data(self): @property def stride(self): - """ Item stride in the base array. """ + """Item stride in the base array.""" if self.base is None: return self.ravel().strides[0] @@ -68,7 +68,7 @@ def stride(self): @property def offset(self): - """ Byte offset in the base array. """ + """Byte offset in the base array.""" return self._extents[0] @@ -105,7 +105,7 @@ def _compute_extents(self, Z): return 0, self.size * self.itemsize def __getitem__(self, key): - """ FIXME: Need to take care of case where key is a list or array """ + """FIXME: Need to take care of case where key is a list or array""" Z = np.ndarray.__getitem__(self, key) if not hasattr(Z, 'shape') or Z.shape == (): @@ -114,7 +114,7 @@ def __getitem__(self, key): return Z def __setitem__(self, key, value): - """ FIXME: Need to take care of case where key is a list or array """ + """FIXME: Need to take care of case where key is a list or array""" Z = np.ndarray.__getitem__(self, key) if Z.shape == (): diff --git a/phy/plot/gloo/parser.py b/phy/plot/gloo/parser.py index 2cceb8f3b..bb8d6be21 100644 --- a/phy/plot/gloo/parser.py +++ b/phy/plot/gloo/parser.py @@ -3,11 +3,10 @@ # Distributed under the (new) BSD License. # ----------------------------------------------------------------------------- -import re import logging +import re from pathlib import Path - log = logging.getLogger(__name__) @@ -16,9 +15,9 @@ def _find(filename): def remove_comments(code): - """ Remove C-style comment from GLSL code string """ + """Remove C-style comment from GLSL code string""" - pattern = r"(\".*?\"|\'.*?\')|(/\*.*?\*/|//[^\r\n]*\n)" + pattern = r'(\".*?\"|\'.*?\')|(/\*.*?\*/|//[^\r\n]*\n)' # first group captures quoted strings (double or single) # second group captures comments (//single-line or /* multi-line */) regex = re.compile(pattern, re.MULTILINE | re.DOTALL) @@ -27,7 +26,7 @@ def do_replace(match): # if the 2nd group (capturing comments) is not None, # it means we have captured a non-quoted (real) comment string. if match.group(2) is not None: - return "" # so we will return empty to remove the comment + return '' # so we will return empty to remove the comment else: # otherwise, we will return the 1st group return match.group(1) # captured quoted-string @@ -35,7 +34,7 @@ def do_replace(match): def remove_version(code): - """ Remove any version directive """ + """Remove any version directive""" pattern = r'\#\s*version[^\r\n]*\n' regex = re.compile(pattern, re.MULTILINE | re.DOTALL) @@ -43,7 +42,7 @@ def remove_version(code): def merge_includes(code): - """ Merge all includes recursively """ + """Merge all includes recursively""" # pattern = '\#\s*include\s*"(?P[a-zA-Z0-9\-\.\/]+)"[^\r\n]*\n' pattern = r'\#\s*include\s*"(?P[a-zA-Z0-9\-\.\/]+)"' @@ -51,18 +50,18 @@ def merge_includes(code): includes = [] def replace(match): - filename = match.group("filename") + filename = match.group('filename') if filename not in includes: includes.append(filename) path = _find(filename) if not path: - log.critical('"%s" not found' % filename) - raise RuntimeError("File not found") - text = '\n// --- start of "%s" ---\n' % filename + log.critical(f'"{filename}" not found') + raise RuntimeError('File not found') + text = f'\n// --- start of "{filename}" ---\n' with open(str(path)) as f: text += remove_comments(f.read()) - text += '// --- end of "%s" ---\n' % filename + text += f'// --- end of "{filename}" ---\n' return text return '' @@ -77,7 +76,7 @@ def replace(match): def preprocess(code): - """ Preprocess a code by removing comments, version and merging includes""" + """Preprocess a code by removing comments, version and merging includes""" if code: # code = remove_comments(code) @@ -86,10 +85,10 @@ def preprocess(code): return code -def get_declarations(code, qualifier=""): - """ Extract declarations of type: +def get_declarations(code, qualifier=''): + """Extract declarations of type: - qualifier type name[,name,...]; + qualifier type name[,name,...]; """ if not len(code): @@ -98,25 +97,35 @@ def get_declarations(code, qualifier=""): variables = [] if isinstance(qualifier, list): - qualifier = "(" + "|".join([str(q) for q in qualifier]) + ")" + qualifier = f'({"|".join([str(q) for q in qualifier])})' - if qualifier != "": - re_type = re.compile(r""" - %s # Variable qualifier + re_type = ( + re.compile( + rf""" + {qualifier} # Variable qualifier \s+(?P\w+) # Variable type \s+(?P[\w,\[\]\n =\.$]+); # Variable name(s) - """ % qualifier, re.VERBOSE) - else: - re_type = re.compile(r""" + """, + re.VERBOSE, + ) + if qualifier != '' + else re.compile( + r""" \s*(?P\w+) # Variable type \s+(?P[\w\[\] ]+) # Variable name(s) - """, re.VERBOSE) + """, + re.VERBOSE, + ) + ) - re_names = re.compile(r""" + re_names = re.compile( + r""" (?P\w+) # Variable name \s*(\[(?P\d+)\])? # Variable size (\s*[^,]+)? - """, re.VERBOSE) + """, + re.VERBOSE, + ) for match in re.finditer(re_type, code): vtype = match.group('type') @@ -129,10 +138,9 @@ def get_declarations(code, qualifier=""): else: size = int(size) if size == 0: - raise RuntimeError( - "Size of a variable array cannot be zero") + raise RuntimeError('Size of a variable array cannot be zero') for i in range(size): - iname = '%s[%d]' % (name, i) + iname = f'{name}[{i}]' variables.append((iname, vtype)) return variables @@ -142,36 +150,39 @@ def get_hooks(code): return [] hooks = [] - re_hooks = re.compile(r"""\<(?P\w+) + re_hooks = re.compile( + r"""\<(?P\w+) (\.(?P.+))? - (\([^<>]+\))?\>""", re.VERBOSE) + (\([^<>]+\))?\>""", + re.VERBOSE, + ) for match in re.finditer(re_hooks, code): hooks.append((match.group('hook'), None)) return list(set(hooks)) def get_args(code): - return get_declarations(code, qualifier="") + return get_declarations(code, qualifier='') def get_externs(code): - return get_declarations(code, qualifier="extern") + return get_declarations(code, qualifier='extern') def get_consts(code): - return get_declarations(code, qualifier="const") + return get_declarations(code, qualifier='const') def get_uniforms(code): - return get_declarations(code, qualifier="uniform") + return get_declarations(code, qualifier='uniform') def get_attributes(code): - return get_declarations(code, qualifier=["attribute", "in"]) + return get_declarations(code, qualifier=['attribute', 'in']) def get_varyings(code): - return get_declarations(code, qualifier="varying") + return get_declarations(code, qualifier='varying') def get_functions(code): @@ -181,28 +192,31 @@ def brace_matcher(n): # after n+1 levels. Matches any string with balanced # braces inside; add the outer braces yourself if needed. # Nongreedy. - return r"[^{}]*?(?:{" * n + r"[^{}]*?" + r"}[^{}]*?)*?" * n + return r'[^{}]*?(?:{' * n + r'[^{}]*?' + r'}[^{}]*?)*?' * n functions = [] - regex = re.compile(r""" + regex = re.compile( + rf""" \s*(?P\w+) # Function return type \s+(?P[\w]+) # Function name \s*\((?P.*?)\) # Function arguments - \s*\{(?P%s)\} # Function content - """ % brace_matcher(5), re.VERBOSE | re.DOTALL) + \s*\{{(?P{brace_matcher(5)})\}} # Function content + """, + re.VERBOSE | re.DOTALL, + ) for match in re.finditer(regex, code): rtype = match.group('type') name = match.group('name') args = match.group('args') fcode = match.group('code') - if name not in ("if", "while"): + if name not in ('if', 'while'): functions.append((rtype, name, args, fcode)) return functions def parse(code): - """ Parse a shader """ + """Parse a shader""" code = preprocess(code) externs = get_externs(code) if code else [] @@ -213,10 +227,12 @@ def parse(code): hooks = get_hooks(code) if code else [] functions = get_functions(code) if code else [] - return {'externs': externs, - 'consts': consts, - 'uniforms': uniforms, - 'attributes': attributes, - 'varyings': varyings, - 'hooks': hooks, - 'functions': functions} + return { + 'externs': externs, + 'consts': consts, + 'uniforms': uniforms, + 'attributes': attributes, + 'varyings': varyings, + 'hooks': hooks, + 'functions': functions, + } diff --git a/phy/plot/gloo/program.py b/phy/plot/gloo/program.py index 242cab289..10fd72833 100644 --- a/phy/plot/gloo/program.py +++ b/phy/plot/gloo/program.py @@ -4,19 +4,18 @@ # ----------------------------------------------------------------------------- import logging -from operator import attrgetter import re +from operator import attrgetter import numpy as np from . import gl -from .snippet import Snippet -from .globject import GLObject from .array import VertexArray -from .buffer import VertexBuffer, IndexBuffer -from .shader import VertexShader, FragmentShader, GeometryShader -from .variable import Uniform, Attribute - +from .buffer import IndexBuffer, VertexBuffer +from .globject import GLObject +from .shader import FragmentShader, GeometryShader, VertexShader +from .snippet import Snippet +from .variable import Attribute, Uniform log = logging.getLogger(__name__) @@ -49,8 +48,9 @@ class Program(GLObject): """ # --------------------------------- - def __init__(self, vertex=None, fragment=None, geometry=None, - count=0, version="120"): + def __init__( + self, vertex=None, fragment=None, geometry=None, count=0, version='120' + ): """ Initialize the program and optionnaly buffer. """ @@ -70,7 +70,7 @@ def __init__(self, vertex=None, fragment=None, geometry=None, self._vertex = vertex self._vertex._version = version else: - log.error("vertex must be a string or a VertexShader") + log.error('vertex must be a string or a VertexShader') if fragment is not None: if isinstance(fragment, str): @@ -79,7 +79,7 @@ def __init__(self, vertex=None, fragment=None, geometry=None, self._fragment = fragment self._fragment._version = version else: - log.error("fragment must be a string or a FragmentShader") + log.error('fragment must be a string or a FragmentShader') if geometry is not None: if isinstance(geometry, str): @@ -88,7 +88,7 @@ def __init__(self, vertex=None, fragment=None, geometry=None, self._geometry = geometry self._geometry._version = version else: - log.error("geometry must be a string or a GeometryShader") + log.error('geometry must be a string or a GeometryShader') self._uniforms = {} self._attributes = {} @@ -101,10 +101,11 @@ def __init__(self, vertex=None, fragment=None, geometry=None, # Build associated structured vertex buffer if count is given if self._count > 0: dtype = [] - for attribute in sorted(self._attributes.values(), filter=attrgetter('name')): + for attribute in sorted( + self._attributes.values(), filter=attrgetter('name') + ): dtype.append(attribute.dtype) - self._buffer = np.zeros( - self._count, dtype=dtype).view(VertexBuffer) + self._buffer = np.zeros(self._count, dtype=dtype).view(VertexBuffer) self.bind(self._buffer) def __len__(self): @@ -115,17 +116,17 @@ def __len__(self): @property def vertex(self): - """ Vertex shader object """ + """Vertex shader object""" return self._vertex @property def fragment(self): - """ Fragment shader object """ + """Fragment shader object""" return self._fragment @property def geometry(self): - """ Geometry shader object """ + """Geometry shader object""" return self._geometry @property @@ -147,13 +148,14 @@ def hooks(self): } """ - return tuple(self._vert_hooks.keys()) + \ - tuple(self._frag_hooks.keys()) + \ - tuple(self._geom_hooks.keys()) + return ( + tuple(self._vert_hooks.keys()) + + tuple(self._frag_hooks.keys()) + + tuple(self._geom_hooks.keys()) + ) def _setup(self): - """ Setup the program by resolving all pending hooks. """ - pass + """Setup the program by resolving all pending hooks.""" def _create(self): """ @@ -162,17 +164,17 @@ def _create(self): A GL context must be available to be able to build (link) """ - log.log(5, "GPU: Creating program") + log.log(5, 'GPU: Creating program') # Check if program has been created if self._handle <= 0: self._handle = gl.glCreateProgram() if not self._handle: - raise ValueError("Cannot create program object") + raise ValueError('Cannot create program object') self._build_shaders(self._handle) - log.log(5, "GPU: Linking program") + log.log(5, 'GPU: Linking program') # Link the program gl.glLinkProgram(self._handle) @@ -197,15 +199,15 @@ def _create(self): attribute.active = False def _build_shaders(self, program): - """ Build and attach shaders """ + """Build and attach shaders""" # Check if we have at least something to attach if not self._vertex: - raise ValueError("No vertex shader has been given") + raise ValueError('No vertex shader has been given') if not self._fragment: - raise ValueError("No fragment shader has been given") + raise ValueError('No fragment shader has been given') - log.log(5, "GPU: Attaching shaders to program") + log.log(5, 'GPU: Attaching shaders to program') # Attach shaders attached = gl.glGetAttachedShaders(program) @@ -220,45 +222,51 @@ def _build_shaders(self, program): shader.activate() if isinstance(shader, GeometryShader): if shader.vertices_out is not None: - gl.glProgramParameteriEXT(self._handle, - gl.GL_GEOMETRY_VERTICES_OUT_EXT, - shader.vertices_out) + gl.glProgramParameteriEXT( + self._handle, + gl.GL_GEOMETRY_VERTICES_OUT_EXT, + shader.vertices_out, + ) if shader.input_type is not None: - gl.glProgramParameteriEXT(self._handle, - gl.GL_GEOMETRY_INPUT_TYPE_EXT, - shader.input_type) + gl.glProgramParameteriEXT( + self._handle, + gl.GL_GEOMETRY_INPUT_TYPE_EXT, + shader.input_type, + ) if shader.output_type is not None: - gl.glProgramParameteriEXT(self._handle, - gl.GL_GEOMETRY_OUTPUT_TYPE_EXT, - shader.output_type) + gl.glProgramParameteriEXT( + self._handle, + gl.GL_GEOMETRY_OUTPUT_TYPE_EXT, + shader.output_type, + ) gl.glAttachShader(program, shader.handle) shader._program = self def _build_hooks(self): - """ Build hooks """ + """Build hooks""" self._vert_hooks = {} self._frag_hooks = {} self._geom_hooks = {} if self._vertex is not None: - for (hook, subhook) in self._vertex.hooks: + for hook, subhook in self._vertex.hooks: self._vert_hooks[hook] = None if self._fragment is not None: - for (hook, subhook) in self._fragment.hooks: + for hook, subhook in self._fragment.hooks: self._frag_hooks[hook] = None if self._geometry is not None: - for (hook, subhook) in self._geometry.hooks: + for hook, subhook in self._geometry.hooks: self._geom_hooks[hook] = None def _build_uniforms(self): - """ Build the uniform objects """ + """Build the uniform objects""" # We might rebuild the program because of snippets but we must # keep already bound uniforms count = 0 - for (name, gtype) in self.all_uniforms: + for name, gtype in self.all_uniforms: if name not in self._uniforms.keys(): uniform = Uniform(self, name, gtype) else: @@ -271,13 +279,13 @@ def _build_uniforms(self): self._need_update = True def _build_attributes(self): - """ Build the attribute objects """ + """Build the attribute objects""" # We might rebuild the program because of snippets but we must # keep already bound attributes dtype = [] - for (name, gtype) in self.all_attributes: + for name, gtype in self.all_attributes: if name not in self._attributes.keys(): attribute = Attribute(self, name, gtype) else: @@ -338,22 +346,24 @@ def __setitem__(self, name, data): self._attributes[name].set_data(data) else: raise IndexError( - "Unknown item %s (no corresponding hook, uniform or attribute)" % name) + f'Unknown item {name} (no corresponding hook, uniform or attribute)' + ) def __getitem__(self, name): if name in self._vert_hooks.keys(): return self._vert_hooks[name] elif name in self._frag_hooks.keys(): return self._frag_hooks[name] -# if name in self._hooks.keys(): -# return self._hooks[name][1] + # if name in self._hooks.keys(): + # return self._hooks[name][1] elif name in self._uniforms.keys(): return self._uniforms[name].data elif name in self._attributes.keys(): return self._attributes[name].data else: raise IndexError( - "Unknown item (no corresponding hook, uniform or attribute)") + 'Unknown item (no corresponding hook, uniform or attribute)' + ) def __contains__(self, name): try: @@ -370,7 +380,7 @@ def __contains__(self, name): def _activate(self): """Activate the program as part of current rendering state.""" - log.log(5, "GPU: Activating program (id=%d)" % self._id) + log.log(5, f'GPU: Activating program (id={self._id})') gl.glUseProgram(self.handle) for uniform in sorted(self._uniforms.values(), key=attrgetter('name')): @@ -393,7 +403,7 @@ def _deactivate(self): # Need fix when dealing with vertex arrays (only need to active the array) for attribute in sorted(self._attributes.values(), key=attrgetter('name')): attribute.deactivate() - log.log(5, "GPU: Deactivating program (id=%d)" % self._id) + log.log(5, f'GPU: Deactivating program (id={self._id})') @property def all_uniforms(self): @@ -447,7 +457,7 @@ def active_uniforms(self): name = m.group('name') if size >= 1: for i in range(size): - name = '%s[%d]' % (m.group('name'), i) + name = f'{m.group("name")}[{i}]' uniforms.append((name, gtype)) else: uniforms.append((name, gtype)) @@ -531,7 +541,7 @@ def active_attributes(self): name = m.group('name') if size >= 1: for i in range(size): - name = '%s[%d]' % (m.group('name'), i) + name = f'{m.group("name")}[{i}]' attributes.append((name, gtype)) else: attributes.append((name, gtype)) @@ -575,7 +585,7 @@ def n_vertices(self): # first=0, count=None): def draw(self, mode=None, indices=None): - """ Draw using the specified mode & indices. + """Draw using the specified mode & indices. :param gl.GLEnum mode: One of @@ -592,7 +602,7 @@ def draw(self, mode=None, indices=None): """ if isinstance(mode, str): - mode = getattr(gl, 'GL_%s' % mode.upper()) + mode = getattr(gl, f'GL_{mode.upper()}') self.activate() attributes = self._attributes.values() @@ -605,9 +615,11 @@ def draw(self, mode=None, indices=None): if isinstance(indices, IndexBuffer): indices.activate() - gltypes = {np.dtype(np.uint8): gl.GL_UNSIGNED_BYTE, - np.dtype(np.uint16): gl.GL_UNSIGNED_SHORT, - np.dtype(np.uint32): gl.GL_UNSIGNED_INT} + gltypes = { + np.dtype(np.uint8): gl.GL_UNSIGNED_BYTE, + np.dtype(np.uint16): gl.GL_UNSIGNED_SHORT, + np.dtype(np.uint32): gl.GL_UNSIGNED_INT, + } gl.glDrawElements(mode, indices.size, gltypes[indices.dtype], None) indices.deactivate() else: diff --git a/phy/plot/gloo/shader.py b/phy/plot/gloo/shader.py index 4849cd7cd..22bc479bd 100644 --- a/phy/plot/gloo/shader.py +++ b/phy/plot/gloo/shader.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Copyright (c) 2009-2016 Nicolas P. Rougier. All rights reserved. # Distributed under the (new) BSD License. @@ -34,12 +33,10 @@ import os.path import re -from .import gl -from .snippet import Snippet +from . import gl from .globject import GLObject -from .parser import (remove_comments, preprocess, - get_uniforms, get_attributes, get_hooks) - +from .parser import get_attributes, get_hooks, get_uniforms, preprocess, remove_comments +from .snippet import Snippet log = logging.getLogger(__name__) @@ -85,7 +82,7 @@ class Shader(GLObject): 'samplerCube': gl.GL_SAMPLER_CUBE, } - def __init__(self, target, code, version="120"): + def __init__(self, target, code, version='120'): """ Initialize the shader. """ @@ -96,7 +93,7 @@ def __init__(self, target, code, version="120"): self._version = version if os.path.isfile(code): - with open(str(code), 'rt') as file: + with open(str(code)) as file: self._code = preprocess(file.read()) self._source = os.path.basename(code) else: @@ -115,21 +112,22 @@ def __setitem__(self, name, snippet): self._snippets[name] = snippet def _replace_hooks(self, name, snippet): - - #re_hook = r"(?P%s)(\.(?P\w+))?" % name - re_hook = r"(?P%s)(\.(?P[\.\w\!]+))?" % name - re_args = r"(\((?P[^<>]+)\))?" + # re_hook = r"(?P%s)(\.(?P\w+))?" % name + re_hook = rf'(?P{name})(\.(?P[\.\w\!]+))?' + re_args = r'(\((?P[^<>]+)\))?' # re_hooks = re.compile("\<" + re_hook + re_args + "\>", re.VERBOSE) - pattern = r"\<" + re_hook + re_args + r"\>" + pattern = r'\<' + re_hook + re_args + r'\>' # snippet is not a Snippet (it should be a string) if not isinstance(snippet, Snippet): + def replace(match): # hook = match.group('hook') subhook = match.group('subhook') if subhook: - return snippet + '.' + subhook + return f'{snippet}.{subhook}' return snippet + self._hooked = re.sub(pattern, replace, self._hooked) return @@ -138,9 +136,9 @@ def replace(match): # Replace expression of type def replace_with_args(match): - #hook = match.group('hook') + # hook = match.group('hook') subhook = match.group('subhook') - #args = match.group('args') + # args = match.group('args') if subhook and '.' in subhook: s = snippet @@ -155,7 +153,7 @@ def replace_with_args(match): # -> A(B(C("t"))) # (t) -> A("t") override = False - if subhook[-1] == "!": + if subhook[-1] == '!': override = True subhook = subhook[:-1] @@ -166,66 +164,66 @@ def replace_with_args(match): # If subhook is a variable (uniform/attribute/varying) if subhook in s.globals: return s.globals[subhook] - return s.mangled_call(subhook, match.group("args"), override=override) + return s.mangled_call(subhook, match.group('args'), override=override) # If subhook is a variable (uniform/attribute/varying) if subhook in snippet.globals: return snippet.globals[subhook] - return snippet.mangled_call(subhook, match.group("args")) + return snippet.mangled_call(subhook, match.group('args')) self._hooked = re.sub(pattern, replace_with_args, self._hooked) def reset(self): - """ Reset shader snippets """ + """Reset shader snippets""" self._snippets = {} @property def code(self): - """ Shader source code (built from original and snippet codes) """ + """Shader source code (built from original and snippet codes)""" # Last minute hook settings self._hooked = self._code for name, snippet in self._snippets.items(): self._replace_hooks(name, snippet) - snippet_code = "// --- Snippets code : start --- //\n" + snippet_code = '// --- Snippets code : start --- //\n' deps = [] for snippet in self._snippets.values(): if isinstance(snippet, Snippet): deps.extend(snippet.dependencies) for snippet in list(set(deps)): snippet_code += snippet.mangled_code() - snippet_code += "// --- Snippets code : end --- //\n" + snippet_code += '// --- Snippets code : end --- //\n' return snippet_code + self._hooked def _create(self): - """ Create the shader """ + """Create the shader""" - log.log(5, "GPU: Creating shader") + log.log(5, 'GPU: Creating shader') # Check if we have something to compile if not self.code: - raise RuntimeError("No code has been given") + raise RuntimeError('No code has been given') # Check that shader object has been created if self._handle <= 0: self._handle = gl.glCreateShader(self._target) if self._handle <= 0: - raise RuntimeError("Cannot create shader object") + raise RuntimeError('Cannot create shader object') def _update(self): - """ Compile the source and checks everything's ok """ + """Compile the source and checks everything's ok""" - log.log(5, "GPU: Compiling shader") + log.log(5, 'GPU: Compiling shader') if len(self.hooks): hooks = [name for name, snippet in self.hooks] - error = "Shader has pending hooks (%s), cannot compile" % hooks + error = f'Shader has pending hooks ({hooks}), cannot compile' raise RuntimeError(error) # Set shader version - code = ("#version %s\n" % self._version) + self.code + code = f'#version {self._version}\n{self.code}' gl.glShaderSource(self._handle, code) # Actual compilation @@ -236,10 +234,10 @@ def _update(self): parsed_errors = self._parse_error(error) for lineno, mesg in parsed_errors: self._print_error(mesg, lineno - 1) - raise RuntimeError("Shader compilation error") + raise RuntimeError('Shader compilation error') def _delete(self): - """ Delete shader from GPU memory (if it was present). """ + """Delete shader from GPU memory (if it was present).""" gl.glDeleteShader(self._handle) @@ -248,15 +246,18 @@ def _delete(self): # 0(7): error C1008: undefined variable "MV" # 0(2) : error C0118: macros prefixed with '__' are reserved re.compile( - r'^\s*(\d+)\((?P\d+)\)\s*:\s(?P.*)', re.MULTILINE), + r'^\s*(\d+)\((?P\d+)\)\s*:\s(?P.*)', re.MULTILINE + ), # ATI / Intel # ERROR: 0:131: '{' : syntax error parse error re.compile( - r'^\s*ERROR:\s(\d+):(?P\d+):\s(?P.*)', re.MULTILINE), + r'^\s*ERROR:\s(\d+):(?P\d+):\s(?P.*)', re.MULTILINE + ), # Nouveau # 0:28(16): error: syntax error, unexpected ')', expecting '(' re.compile( - r'^\s*(\d+):(?P\d+)\((\d+)\):\s(?P.*)', re.MULTILINE) + r'^\s*(\d+):(?P\d+)\((\d+)\):\s(?P.*)', re.MULTILINE + ), ] def _parse_error(self, error): @@ -272,11 +273,12 @@ def _parse_error(self, error): for error_re in self._ERROR_RE: matches = list(error_re.finditer(error)) if matches: - errors = [(int(m.group('line_no')), m.group('error_msg')) - for m in matches] + errors = [ + (int(m.group('line_no')), m.group('error_msg')) for m in matches + ] return sorted(errors, key=lambda elem: elem[0]) else: - raise ValueError('Unknown GLSL error format:\n{}\n'.format(error)) + raise ValueError(f'Unknown GLSL error format:\n{error}\n') def _print_error(self, error, lineno): """ @@ -294,24 +296,24 @@ def _print_error(self, error, lineno): start = max(0, lineno - 3) end = min(len(lines), lineno + 3) - print('Error in %s' % (repr(self))) - print(' -> %s' % error) + print(f'Error in {repr(self)}') + print(f' -> {error}') print() if start > 0: print(' ...') for i, line in enumerate(lines[start:end]): if (i + start) == lineno: - print(' %03d %s' % (i + start, line)) + print(f' {i + start:03d} {line}') else: if len(line): - print(' %03d %s' % (i + start, line)) + print(f' {i + start:03d} {line}') if end < len(lines): print(' ...') print() @property def hooks(self): - """ Shader hooks (place where snippets can be inserted) """ + """Shader hooks (place where snippets can be inserted)""" # We get hooks from the original code, not the hooked one code = remove_comments(self._hooked) @@ -319,7 +321,7 @@ def hooks(self): @property def uniforms(self): - """ Shader uniforms obtained from source code """ + """Shader uniforms obtained from source code""" code = remove_comments(self.code) gtypes = Shader._gtypes @@ -327,7 +329,7 @@ def uniforms(self): @property def attributes(self): - """ Shader attributes obtained from source code """ + """Shader attributes obtained from source code""" code = remove_comments(self.code) gtypes = Shader._gtypes @@ -336,58 +338,64 @@ def attributes(self): # ------------------------------------------------------ VertexShader class --- class VertexShader(Shader): - """ Vertex shader class """ + """Vertex shader class""" - def __init__(self, code=None, version="120"): + def __init__(self, code=None, version='120'): Shader.__init__(self, gl.GL_VERTEX_SHADER, code, version) @property def code(self): - code = super(VertexShader, self).code - code = "#define _GLUMPY__VERTEX_SHADER__\n" + code + code = super().code + code = f'#define _GLUMPY__VERTEX_SHADER__\n{code}' return code def __repr__(self): - return "Vertex shader %d (%s)" % (self._id, self._source) + return f'Vertex shader {self._id} ({self._source})' class FragmentShader(Shader): - """ Fragment shader class """ + """Fragment shader class""" - def __init__(self, code=None, version="120"): + def __init__(self, code=None, version='120'): Shader.__init__(self, gl.GL_FRAGMENT_SHADER, code, version) @property def code(self): - code = super(FragmentShader, self).code - code = "#define _GLUMPY__FRAGMENT_SHADER__\n" + code + code = super().code + code = f'#define _GLUMPY__FRAGMENT_SHADER__\n{code}' return code def __repr__(self): - return "Fragment shader %d (%s)" % (self._id, self._source) + return f'Fragment shader {self._id} ({self._source})' class GeometryShader(Shader): - """ Geometry shader class. + """Geometry shader class. - :param str code: Shader code or a filename containing shader code - :param int vertices_out: Number of output vertices - :param gl.GLEnum input_type: + :param str code: Shader code or a filename containing shader code + :param int vertices_out: Number of output vertices + :param gl.GLEnum input_type: - * GL_POINTS - * GL_LINES​, GL_LINE_STRIP​, GL_LINE_LIST - * GL_LINES_ADJACENCY​, GL_LINE_STRIP_ADJACENCY - * GL_TRIANGLES​, GL_TRIANGLE_STRIP​, GL_TRIANGLE_FAN - * GL_TRIANGLES_ADJACENCY​, GL_TRIANGLE_STRIP_ADJACENCY + * GL_POINTS + * GL_LINES​, GL_LINE_STRIP​, GL_LINE_LIST + * GL_LINES_ADJACENCY​, GL_LINE_STRIP_ADJACENCY + * GL_TRIANGLES​, GL_TRIANGLE_STRIP​, GL_TRIANGLE_FAN + * GL_TRIANGLES_ADJACENCY​, GL_TRIANGLE_STRIP_ADJACENCY - :param gl.GLEnum output_type: + :param gl.GLEnum output_type: - * GL_POINTS, GL_LINES​, GL_LINE_STRIP - * GL_TRIANGLES​, GL_TRIANGLE_STRIP​, GL_TRIANGLE_FAN + * GL_POINTS, GL_LINES​, GL_LINE_STRIP + * GL_TRIANGLES​, GL_TRIANGLE_STRIP​, GL_TRIANGLE_FAN """ - def __init__(self, code=None, - vertices_out=None, input_type=None, output_type=None, version="120"): + def __init__( + self, + code=None, + vertices_out=None, + input_type=None, + output_type=None, + version='120', + ): Shader.__init__(self, gl.GL_GEOMETRY_SHADER_EXT, code, version) self._vertices_out = vertices_out @@ -429,4 +437,4 @@ def output_type(self, value): self._output_type = value def __repr__(self): - return "Geometry shader %d (%s)" % (self._id, self._source) + return f'Geometry shader {self._id} ({self._source})' diff --git a/phy/plot/gloo/snippet.py b/phy/plot/gloo/snippet.py index 720d064c2..7e3d4f5ef 100644 --- a/phy/plot/gloo/snippet.py +++ b/phy/plot/gloo/snippet.py @@ -9,7 +9,7 @@ from . import parser -class Snippet(object): +class Snippet: """ A snippet is a piece of GLSL code that can be injected into an another GLSL code. It provides the necessary machinery to take care of name collisions, @@ -53,7 +53,6 @@ class Snippet(object): aliases = {} def __init__(self, code=None, default=None, *args, **kwargs): - # Original source code self._source_code = parser.merge_includes(code) @@ -63,7 +62,7 @@ def __init__(self, code=None, default=None, *args, **kwargs): # Arguments (other snippets or strings) for arg in args: if isinstance(arg, Snippet) and self in arg.snippets: - raise ValueError("Recursive call is forbidden.") + raise ValueError('Recursive call is forbidden.') self._args = list(args) # No chained snippet yet @@ -85,20 +84,20 @@ def __init__(self, code=None, default=None, *args, **kwargs): # If no name has been given, set a default one if self._name is None: classname = self.__class__.__name__ - self._name = "%s_%d" % (classname, self._id) + self._name = f'{classname}_{self._id}' # Symbol table self._symbols = {} - for (name, dtype) in self._objects["attributes"]: - self._symbols[name] = "%s_%d" % (name, self._id) - for (name, dtype) in self._objects["uniforms"]: - self._symbols[name] = "%s_%d" % (name, self._id) - for (name, dtype) in self._objects["varyings"]: - self._symbols[name] = "%s_%d" % (name, self._id) - for (name, dtype) in self._objects["consts"]: - self._symbols[name] = "%s_%d" % (name, self._id) - for (rtype, name, args, code) in self._objects["functions"]: - self._symbols[name] = "%s_%d" % (name, self._id) + for name, dtype in self._objects['attributes']: + self._symbols[name] = f'{name}_{self._id}' + for name, dtype in self._objects['uniforms']: + self._symbols[name] = f'{name}_{self._id}' + for name, dtype in self._objects['varyings']: + self._symbols[name] = f'{name}_{self._id}' + for name, dtype in self._objects['consts']: + self._symbols[name] = f'{name}_{self._id}' + for rtype, name, args, code in self._objects['functions']: + self._symbols[name] = f'{name}_{self._id}' # Aliases (through kwargs) for name, alias in kwargs.items(): @@ -108,25 +107,25 @@ def __init__(self, code=None, default=None, *args, **kwargs): self._programs = [] def process_kwargs(self, **kwargs): - """ Process kwargs as given in __init__() or __call__() """ + """Process kwargs as given in __init__() or __call__()""" - if "name" in kwargs.keys(): - self._name = kwargs["name"] - del kwargs["name"] + if 'name' in kwargs: + self._name = kwargs['name'] + del kwargs['name'] - if "call" in kwargs.keys(): - self._call = kwargs["call"] - del kwargs["call"] + if 'call' in kwargs: + self._call = kwargs['call'] + del kwargs['call'] @property def name(self): - """ Name of the snippet """ + """Name of the snippet""" return self._name @property def programs(self): - """ Currently attached programs """ + """Currently attached programs""" return self._programs @@ -157,7 +156,9 @@ def locals(self): symbols = {} objects = self._objects - for name, dtype in objects["uniforms"] + objects["attributes"] + objects["varyings"]: + for name, dtype in ( + objects['uniforms'] + objects['attributes'] + objects['varyings'] + ): symbols[name] = self.symbols[name] # return self._symbols return symbols @@ -178,13 +179,13 @@ def globals(self): @property def args(self): - """ Call arguments """ + """Call arguments""" return list(self._args) @property def next(self): - """ Next snippet in the arihmetic chain. """ + """Next snippet in the arihmetic chain.""" if self._next: return self._next[1] @@ -220,7 +221,9 @@ def snippets(self): D.snippets # [A,B,C] """ - all = [self, ] + all = [ + self, + ] for snippet in self._args: if isinstance(snippet, Snippet): all.extend(snippet.snippets) @@ -323,34 +326,33 @@ def dependencies(self): @property def code(self): - """ Mangled code """ + """Mangled code""" - code = "" + code = '' for snippet in self.dependencies: code += snippet.mangled_code() return code def mangled_code(self): - """ Generate mangled code """ + """Generate mangled code""" code = self._source_code objects = self._objects - functions = objects["functions"] - names = objects["uniforms"] + \ - objects["attributes"] + objects["varyings"] + functions = objects['functions'] + names = objects['uniforms'] + objects['attributes'] + objects['varyings'] for _, name, _, _ in functions: symbol = self.symbols[name] - code = re.sub(r"(?<=[^\w])(%s)(?=\()" % name, symbol, code) + code = re.sub(rf'(?<=[^\w])({name})(?=\()', symbol, code) for name, _ in names: # Variable starting "__" are protected and unaliased # if not name.startswith("__"): symbol = self.symbols[name] - code = re.sub(r"(?<=[^\w])(%s)(?=[^\w])" % name, symbol, code) + code = re.sub(rf'(?<=[^\w])({name})(?=[^\w])', symbol, code) return code @property def call(self): - """ Computes and returns the GLSL code that correspond to the call """ + """Computes and returns the GLSL code that correspond to the call""" self.mangled_code() return self.mangled_call() @@ -364,13 +366,12 @@ def mangled_call(self, function=None, arguments=None, override=False): with shader arguments """ - s = "" + s = '' # Is there a function defined in the snippet ? # (It may happen a snippet only has uniforms, like the Viewport snippet) # WARN: what about Viewport(Transform) ? - if len(self._objects["functions"]): - + if len(self._objects['functions']): # Is there a function specified in the shader source ? # Such as if function: @@ -381,12 +382,12 @@ def mangled_call(self, function=None, arguments=None, override=False): elif self._call is not None: name = self._call else: - _, name, _, _ = self._objects["functions"][0] + _, name, _, _ = self._objects['functions'][0] s = self.lookup(name, deepsearch=False) or name if len(self._args) and override is False: - s += "(" + s += '(' for i, arg in enumerate(self._args): if isinstance(arg, Snippet): # We do not propagate given function to to other snippets @@ -399,27 +400,27 @@ def mangled_call(self, function=None, arguments=None, override=False): s += str(arg) if i < (len(self._args) - 1): - s += ", " - s += ")" + s += ', ' + s += ')' else: # If an argument has been given, we put it at the end # This handles hooks of the form if arguments is not None: - s += "(%s)" % arguments + s += f'({arguments})' else: - s += "()" + s += '()' if self.next: operand, other = self._next - if operand in "+-/*": + if operand in '+-/*': call = other.mangled_call(function, arguments).strip() if len(call): - s += ' ' + operand + ' ' + call + s += f' {operand} {call}' # No function defined in this snippet, we look for next one else: if self._next: operand, other = self.next - if operand in "+-/*": + if operand in '+-/*': s = other.mangled_call(function, arguments) return s @@ -432,7 +433,7 @@ def __call__(self, *args, **kwargs): for arg in args: if isinstance(arg, Snippet) and self in arg.snippets: - raise ValueError("Recursive call is forbidden") + raise ValueError('Recursive call is forbidden') # Override call arguments self._args = args @@ -447,12 +448,9 @@ def __call__(self, *args, **kwargs): return self def copy(self, deep=False): - """ Shallow or deep copy of the snippet """ + """Shallow or deep copy of the snippet""" - if deep: - snippet = copy.deepcopy(self) - else: - snippet = copy.copy(self) + snippet = copy.deepcopy(self) if deep else copy.copy(self) return snippet def __op__(self, operand, other): @@ -461,54 +459,54 @@ def __op__(self, operand, other): return snippet def __add__(self, other): - return self.__op__("+", other) + return self.__op__('+', other) def __and__(self, other): - return self.__op__("&", other) + return self.__op__('&', other) def __sub__(self, other): - return self.__op__("-", other) + return self.__op__('-', other) def __mul__(self, other): - return self.__op__("*", other) + return self.__op__('*', other) def __div__(self, other): - return self.__op__("/", other) + return self.__op__('/', other) def __radd__(self, other): - return self.__op__("+", other) + return self.__op__('+', other) def __rand__(self, other): - return self.__op__("&", other) + return self.__op__('&', other) def __rsub__(self, other): - return self.__op__("-", other) + return self.__op__('-', other) def __rmul__(self, other): - return self.__op__("*", other) + return self.__op__('*', other) def __rdiv__(self, other): - return self.__op__("/", other) + return self.__op__('/', other) def __rshift__(self, other): - return self.__op__(";", other) + return self.__op__(';', other) def __repr__(self): # return self.generate_call() s = self._name # s = self.__class__.__name__ - s += "(" + s += '(' if len(self._args): - s += " " + s += ' ' for i, snippet in enumerate(self._args): s += repr(snippet) if i < len(self._args) - 1: - s += ", " - s += " " - s += ")" + s += ', ' + s += ' ' + s += ')' if self._next: - s += " %s %s" % self._next + s += ' {} {}'.format(*self._next) return s @@ -573,5 +571,5 @@ def __setitem__(self, key, value): found = True if not found: - error = 'Snippet does not have such key ("%s")' % key + error = f'Snippet does not have such key ("{key}")' raise IndexError(error) diff --git a/phy/plot/gloo/texture.py b/phy/plot/gloo/texture.py index 1d9964e0f..852a5f9cf 100644 --- a/phy/plot/gloo/texture.py +++ b/phy/plot/gloo/texture.py @@ -33,38 +33,30 @@ import numpy as np from . import gl -from .gpudata import GPUData from .globject import GLObject - +from .gpudata import GPUData log = logging.getLogger(__name__) class Texture(GPUData, GLObject): - """ Generic texture """ - - _cpu_formats = {1: gl.GL_RED, - 2: gl.GL_RG, - 3: gl.GL_RGB, - 4: gl.GL_RGBA} - - _gpu_formats = {1: gl.GL_RED, - 2: gl.GL_RG, - 3: gl.GL_RGB, - 4: gl.GL_RGBA} - - _gpu_float_formats = {1: gl.GL_R32F, - 2: gl.GL_RG32F, - 3: gl.GL_RGB32F, - 4: gl.GL_RGBA32F} - - _gtypes = {np.dtype(np.int8): gl.GL_BYTE, - np.dtype(np.uint8): gl.GL_UNSIGNED_BYTE, - np.dtype(np.int16): gl.GL_SHORT, - np.dtype(np.uint16): gl.GL_UNSIGNED_SHORT, - np.dtype(np.int32): gl.GL_INT, - np.dtype(np.uint32): gl.GL_UNSIGNED_INT, - np.dtype(np.float32): gl.GL_FLOAT} + """Generic texture""" + + _cpu_formats = {1: gl.GL_RED, 2: gl.GL_RG, 3: gl.GL_RGB, 4: gl.GL_RGBA} + + _gpu_formats = {1: gl.GL_RED, 2: gl.GL_RG, 3: gl.GL_RGB, 4: gl.GL_RGBA} + + _gpu_float_formats = {1: gl.GL_R32F, 2: gl.GL_RG32F, 3: gl.GL_RGB32F, 4: gl.GL_RGBA32F} + + _gtypes = { + np.dtype(np.int8): gl.GL_BYTE, + np.dtype(np.uint8): gl.GL_UNSIGNED_BYTE, + np.dtype(np.int16): gl.GL_SHORT, + np.dtype(np.uint16): gl.GL_UNSIGNED_SHORT, + np.dtype(np.int32): gl.GL_INT, + np.dtype(np.uint32): gl.GL_UNSIGNED_INT, + np.dtype(np.float32): gl.GL_FLOAT, + } def __init__(self, target): GLObject.__init__(self) @@ -76,22 +68,24 @@ def __init__(self, target): self._gpu_format = None def _check_shape(self, shape, ndims): - """ Check and normalize shape. """ + """Check and normalize shape.""" if len(shape) < ndims: - raise ValueError("Too few dimensions for texture") + raise ValueError('Too few dimensions for texture') elif len(shape) > ndims + 1: - raise ValueError("Too many dimensions for texture") + raise ValueError('Too many dimensions for texture') elif len(shape) == ndims: - shape = list(shape) + [1, ] + shape = list(shape) + [ + 1, + ] elif len(shape) == ndims + 1: if shape[-1] > 4: - raise ValueError("Too many channels for texture") + raise ValueError('Too many channels for texture') return shape @property def need_update(self): - """ Whether object needs to be updated """ + """Whether object needs to be updated""" return self.pending_data is not None @@ -127,7 +121,7 @@ def gpu_format(self): @gpu_format.setter def gpu_format(self, value): - """ Texture GPU format. """ + """Texture GPU format.""" self._gpu_format = value self._need_setup = True @@ -137,32 +131,32 @@ def gtype(self): if self.dtype in Texture._gtypes.keys(): return Texture._gtypes[self.dtype] else: - raise TypeError("No available GL type equivalent") + raise TypeError('No available GL type equivalent') @property def wrapping(self): - """ Texture wrapping mode """ + """Texture wrapping mode""" return self._wrapping @wrapping.setter def wrapping(self, value): - """ Texture wrapping mode """ + """Texture wrapping mode""" self._wrapping = value self._need_setup = True @property def interpolation(self): - """ Texture interpolation for minification and magnification. """ + """Texture interpolation for minification and magnification.""" return self._interpolation @interpolation.setter def interpolation(self, value): - """ Texture interpolation for minication and magnification. """ + """Texture interpolation for minication and magnification.""" if isinstance(value, str): - value = getattr(gl, 'GL_%s' % value.upper()) + value = getattr(gl, f'GL_{value.upper()}') if isinstance(value, (list, tuple)): self._interpolation = value @@ -172,11 +166,11 @@ def interpolation(self, value): def set_interpolation(self, value): if isinstance(value, str): - value = getattr(gl, 'GL_%s' % value.upper()) + value = getattr(gl, f'GL_{value.upper()}') self._interpolation = value, value def _setup(self): - """ Setup texture on GPU """ + """Setup texture on GPU""" min_filter, mag_filter = self._interpolation wrapping = self._wrapping @@ -185,39 +179,38 @@ def _setup(self): gl.glTexParameterf(self.target, gl.GL_TEXTURE_MAG_FILTER, mag_filter) gl.glTexParameterf(self.target, gl.GL_TEXTURE_WRAP_S, wrapping) gl.glTexParameterf(self.target, gl.GL_TEXTURE_WRAP_T, wrapping) - gl.glTexParameterf(self.target, gl.GL_TEXTURE_WRAP_R, - gl.GL_CLAMP_TO_EDGE) + gl.glTexParameterf(self.target, gl.GL_TEXTURE_WRAP_R, gl.GL_CLAMP_TO_EDGE) self._need_setup = False def _activate(self): - """ Activate texture on GPU """ + """Activate texture on GPU""" - log.log(5, "GPU: Activate texture") + log.log(5, 'GPU: Activate texture') gl.glBindTexture(self.target, self._handle) if self._need_setup: self._setup() def _deactivate(self): - """ Deactivate texture on GPU """ + """Deactivate texture on GPU""" - log.log(5, "GPU: Deactivate texture") + log.log(5, 'GPU: Deactivate texture') gl.glBindTexture(self._target, 0) def _create(self): - """ Create texture on GPU """ + """Create texture on GPU""" - log.log(5, "GPU: Creating texture") + log.log(5, 'GPU: Creating texture') self._handle = gl.glGenTextures(1) def _delete(self): - """ Delete texture from GPU """ + """Delete texture from GPU""" - log.log(5, "GPU: Deleting texture") + log.log(5, 'GPU: Deleting texture') if self.handle > -1: gl.glDeleteTextures(np.array([self.handle], dtype=np.uint32)) def get(self): - """ Read the texture data back into CPU memory """ + """Read the texture data back into CPU memory""" host = np.zeros(self.shape, self.dtype) gl.glBindTexture(self.target, self._handle) gl.glGetTexImage(self.target, 0, self.cpu_format, self.gtype, host) @@ -244,25 +237,24 @@ def width(self): return self.shape[0] def _setup(self): - """ Setup texture on GPU """ + """Setup texture on GPU""" Texture._setup(self) gl.glBindTexture(self.target, self._handle) - gl.glTexImage1D(self.target, 0, self._gpu_format, self.width, - 0, self._cpu_format, self.gtype, None) + gl.glTexImage1D( + self.target, 0, self._gpu_format, self.width, 0, self._cpu_format, self.gtype, None + ) self._need_setup = False def _update(self): - - log.log(5, "GPU: Updating texture") + log.log(5, 'GPU: Updating texture') if self.pending_data: start, stop = self.pending_data offset, nbytes = start, stop - start itemsize = self.strides[0] x = offset // itemsize width = nbytes // itemsize - gl.glTexSubImage1D(self.target, 0, x, width, - self._cpu_format, self.gtype, self) + gl.glTexSubImage1D(self.target, 0, x, width, self._cpu_format, self.gtype, self) self._pending_data = None self._need_update = False @@ -279,7 +271,7 @@ def __init__(self): class Texture2D(Texture): - """ 2D texture """ + """2D texture""" def __init__(self): Texture.__init__(self, gl.GL_TEXTURE_2D) @@ -289,33 +281,42 @@ def __init__(self): @property def width(self): - """ Texture width """ + """Texture width""" return self.shape[1] @property def height(self): - """ Texture height """ + """Texture height""" return self.shape[0] def _setup(self): - """ Setup texture on GPU """ + """Setup texture on GPU""" Texture._setup(self) gl.glBindTexture(self.target, self._handle) - gl.glTexImage2D(self.target, 0, self._gpu_format, self.width, self.height, - 0, self._cpu_format, self.gtype, None) + gl.glTexImage2D( + self.target, + 0, + self._gpu_format, + self.width, + self.height, + 0, + self._cpu_format, + self.gtype, + None, + ) self._need_setup = False def _update(self): - """ Update texture on GPU """ + """Update texture on GPU""" if self.width == 0: return if self.pending_data: - log.log(5, "GPU: Updating texture") + log.log(5, 'GPU: Updating texture') start, stop = self.pending_data offset, nbytes = start, stop - start @@ -326,8 +327,7 @@ def _update(self): nbytes += offset % self.width offset -= offset % self.width - nbytes += (self.width - ((offset + nbytes) % - self.width)) % self.width + nbytes += (self.width - ((offset + nbytes) % self.width)) % self.width # x = 0 # y = offset // self.width @@ -338,8 +338,17 @@ def _update(self): # self._cpu_format, self.gtype, self) # HACK: disable partial texture update which fails if the new texture # doesn't have the same size. - gl.glTexImage2D(self.target, 0, self._gpu_format, self.width, self.height, - 0, self._cpu_format, self.gtype, self) + gl.glTexImage2D( + self.target, + 0, + self._gpu_format, + self.width, + self.height, + 0, + self._cpu_format, + self.gtype, + self, + ) gl.glBindTexture(self._target, self.handle) self._pending_data = None @@ -347,7 +356,7 @@ def _update(self): class TextureFloat2D(Texture2D): - """ 2D float texture """ + """2D float texture""" def __init__(self): Texture2D.__init__(self) @@ -355,7 +364,7 @@ def __init__(self): class DepthTexture(Texture2D): - """ Depth texture """ + """Depth texture""" def __init__(self): Texture2D.__init__(self) @@ -364,13 +373,12 @@ def __init__(self): class TextureCube(Texture): - """ Cube texture """ + """Cube texture""" def __init__(self): - Texture.__init__(self, gl.GL_TEXTURE_CUBE_MAP) if self.shape[0] != 6: - error = "Texture cube require arrays first dimension to be 6" + error = 'Texture cube require arrays first dimension to be 6' log.error(error) raise RuntimeError(error) @@ -380,46 +388,59 @@ def __init__(self): @property def width(self): - """ Texture width """ + """Texture width""" return self.shape[2] @property def height(self): - """ Texture height """ + """Texture height""" return self.shape[1] def _setup(self): - """ Setup texture on GPU """ + """Setup texture on GPU""" Texture._setup(self) gl.glEnable(gl.GL_TEXTURE_CUBE_MAP) gl.glBindTexture(self.target, self._handle) - targets = [gl.GL_TEXTURE_CUBE_MAP_POSITIVE_X, - gl.GL_TEXTURE_CUBE_MAP_NEGATIVE_X, - gl.GL_TEXTURE_CUBE_MAP_POSITIVE_Y, - gl.GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, - gl.GL_TEXTURE_CUBE_MAP_POSITIVE_Z, - gl.GL_TEXTURE_CUBE_MAP_NEGATIVE_Z] + targets = [ + gl.GL_TEXTURE_CUBE_MAP_POSITIVE_X, + gl.GL_TEXTURE_CUBE_MAP_NEGATIVE_X, + gl.GL_TEXTURE_CUBE_MAP_POSITIVE_Y, + gl.GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, + gl.GL_TEXTURE_CUBE_MAP_POSITIVE_Z, + gl.GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, + ] for i, target in enumerate(targets): - gl.glTexImage2D(target, 0, self._gpu_format, self.width, self.height, - 0, self._cpu_format, self.gtype, None) + gl.glTexImage2D( + target, + 0, + self._gpu_format, + self.width, + self.height, + 0, + self._cpu_format, + self.gtype, + None, + ) self._need_setup = False def _update(self): - log.log(5, "GPU: Updating texture cube") + log.log(5, 'GPU: Updating texture cube') if self.need_update: gl.glEnable(gl.GL_TEXTURE_CUBE_MAP) gl.glBindTexture(self.target, self.handle) - targets = [gl.GL_TEXTURE_CUBE_MAP_POSITIVE_X, - gl.GL_TEXTURE_CUBE_MAP_NEGATIVE_X, - gl.GL_TEXTURE_CUBE_MAP_POSITIVE_Y, - gl.GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, - gl.GL_TEXTURE_CUBE_MAP_POSITIVE_Z, - gl.GL_TEXTURE_CUBE_MAP_NEGATIVE_Z] + targets = [ + gl.GL_TEXTURE_CUBE_MAP_POSITIVE_X, + gl.GL_TEXTURE_CUBE_MAP_NEGATIVE_X, + gl.GL_TEXTURE_CUBE_MAP_POSITIVE_Y, + gl.GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, + gl.GL_TEXTURE_CUBE_MAP_POSITIVE_Z, + gl.GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, + ] for i, target in enumerate(targets): face = self[i] @@ -439,30 +460,30 @@ def _update(self): nbytes /= itemsize nbytes += offset % self.width offset -= offset % self.width - nbytes += (self.width - ((offset + nbytes) % - self.width)) % self.width + nbytes += (self.width - ((offset + nbytes) % self.width)) % self.width x = 0 y = offset // self.width width = self.width height = nbytes // self.width - gl.glTexSubImage2D(target, 0, x, y, width, height, - self._cpu_format, self.gtype, face) + gl.glTexSubImage2D( + target, 0, x, y, width, height, self._cpu_format, self.gtype, face + ) self._pending_data = None self._need_update = False def _activate(self): - """ Activate texture on GPU """ + """Activate texture on GPU""" - log.log(5, "GPU: Activate texture cube") + log.log(5, 'GPU: Activate texture cube') gl.glEnable(gl.GL_TEXTURE_CUBE_MAP) gl.glBindTexture(self.target, self._handle) if self._need_setup: self._setup() def _deactivate(self): - """ Deactivate texture on GPU """ + """Deactivate texture on GPU""" - log.log(5, "GPU: Deactivate texture cube") + log.log(5, 'GPU: Deactivate texture cube') gl.glBindTexture(self._target, 0) gl.glDisable(gl.GL_TEXTURE_CUBE_MAP) diff --git a/phy/plot/gloo/uniforms.py b/phy/plot/gloo/uniforms.py index 53d2ff8e0..2c015f1ae 100644 --- a/phy/plot/gloo/uniforms.py +++ b/phy/plot/gloo/uniforms.py @@ -2,8 +2,7 @@ # Copyright (c) 2009-2016 Nicolas P. Rougier. All rights reserved. # Distributed under the (new) BSD License. # ----------------------------------------------------------------------------- -""" -""" +""" """ from functools import reduce from operator import mul @@ -31,14 +30,8 @@ def dtype_reduce(dtype, level=0, depth=0): # No fields if fields is None: - if dtype.shape: - count = reduce(mul, dtype.shape) - else: - count = 1 - if dtype.subdtype: - name = str(dtype.subdtype[0]) - else: - name = str(dtype) + count = reduce(mul, dtype.shape) if dtype.shape else 1 + name = str(dtype.subdtype[0]) if dtype.subdtype else str(dtype) return ['', count, name] else: items = [] @@ -50,7 +43,7 @@ def dtype_reduce(dtype, level=0, depth=0): items.append([key, l[1], l[2]]) else: items.append(l) - name += key + ',' + name += f'{key},' # Check if we can reduce item list ctype = None @@ -93,14 +86,13 @@ class Uniforms(Texture2D): """ def __init__(self, size, dtype): - """ Initialization """ + """Initialization""" # Check dtype is made of float32 only dtype = eval(str(np.dtype(dtype))) rtype = dtype_reduce(dtype) if type(rtype[0]) is not str or rtype[2] != 'float32': - raise RuntimeError( - "Uniform type cannot be reduced to float32 only") + raise RuntimeError('Uniform type cannot be reduced to float32 only') # True dtype (the one given in args) self._original_dtype = np.dtype(dtype) @@ -131,36 +123,37 @@ def __init__(self, size, dtype): if size % cols: rows += 1 - Texture2D.__init__(self, shape=(rows, cols, 4), dtype=np.float32, - resizeable=False, store=True) + Texture2D.__init__( + self, shape=(rows, cols, 4), dtype=np.float32, resizeable=False, store=True + ) data = self._data.ravel() self._typed_data = data.view(self._complete_dtype) self._size = size def __setitem__(self, key, value): - """ x.__getitem__(y) <==> x[y] """ + """x.__getitem__(y) <==> x[y]""" if self.base is not None and not self._valid: - raise ValueError("This uniforms view has been invalited") + raise ValueError('This uniforms view has been invalited') size = self._size if isinstance(key, int): if key < 0: key += size if key < 0 or key > size: - raise IndexError("Uniforms assignment index out of range") + raise IndexError('Uniforms assignment index out of range') start, stop = key, key + 1 elif isinstance(key, slice): start, stop, step = key.indices(size) if step != 1: - raise ValueError("Cannot access non-contiguous uniforms data") + raise ValueError('Cannot access non-contiguous uniforms data') if stop < start: start, stop = stop, start elif key == Ellipsis: start = 0 stop = size else: - raise TypeError("Uniforms indices must be integers") + raise TypeError('Uniforms indices must be integers') # First we set item using the typed data # shape = self._typed_data[start:stop].shape @@ -181,10 +174,10 @@ def __setitem__(self, key, value): stop = stop[0], self.shape[1] - 1 offset = start[0], start[1], 0 - data = self._data[start[0]:stop[0] + 1, start[1]:stop[1]] + data = self._data[start[0] : stop[0] + 1, start[1] : stop[1]] self.set_data(data=data, offset=offset, copy=False) - def code(self, prefix="u_"): + def code(self, prefix='u_'): """ Generate the GLSL code needed to retrieve fake uniform values from a texture. The generated uniform names can be prefixed with the given prefix. @@ -196,19 +189,18 @@ def code(self, prefix="u_"): header = """uniform sampler2D u_uniforms;\n""" # Header generation (easy) - types = {1: 'float', 2: 'vec2 ', 3: 'vec3 ', - 4: 'vec4 ', 9: 'mat3 ', 16: 'mat4 '} + types = {1: 'float', 2: 'vec2 ', 3: 'vec3 ', 4: 'vec4 ', 9: 'mat3 ', 16: 'mat4 '} for name, count, _ in _dtype: - header += "varying %s %s%s;\n" % (types[count], prefix, name) + header += f'varying {types[count]} {prefix}{name};\n' # Body generation (not so easy) rows, cols = self.shape[0], self.shape[1] count = self._complete_count - body = """\nvoid fetch_uniforms(float index) { - float rows = %.1f; - float cols = %.1f; - float count = %.1f; + body = f"""\nvoid fetch_uniforms(float index) {{ + float rows = {rows:.1f}; + float cols = {cols:.1f}; + float count = {count:.1f}; int index_x = int(mod(index, (floor(cols/(count/4.0))))) * int(count/4.0); int index_y = int(floor(index / (floor(cols/(count/4.0))))); float size_x = cols - 1.0; @@ -217,7 +209,7 @@ def code(self, prefix="u_"): if (size_y > 0.0) ty = float(index_y)/size_y; int i = index_x; - vec4 _uniform;\n""" % (rows, cols, count) + vec4 _uniform;\n""" _dtype = {name: count for name, count, _ in _dtype} store = 0 @@ -226,28 +218,27 @@ def code(self, prefix="u_"): count, shift = _dtype[name], 0 while count: if store == 0: - body += "\n _uniform = texture2D(u_uniforms, vec2(float(i++)/size_x,ty));\n" + body += '\n _uniform = texture2D(u_uniforms, vec2(float(i++)/size_x,ty));\n' store = 4 if store == 4: - a = "xyzw" + a = 'xyzw' elif store == 3: - a = "yzw" + a = 'yzw' elif store == 2: - a = "zw" + a = 'zw' elif store == 1: - a = "w" + a = 'w' if shift == 0: - b = "xyzw" + b = 'xyzw' elif shift == 1: - b = "yzw" + b = 'yzw' elif shift == 2: - b = "zw" + b = 'zw' elif shift == 3: - b = "w" + b = 'w' i = min(min(len(b), count), len(a)) - body += " %s%s.%s = _uniforms.%s;\n" % ( - prefix, name, b[:i], a[:i]) + body += f' {prefix}{name}.{b[:i]} = _uniforms.{a[:i]};\n' count -= i shift += i store -= i diff --git a/phy/plot/gloo/variable.py b/phy/plot/gloo/variable.py index c3f4b6b8a..64e185134 100644 --- a/phy/plot/gloo/variable.py +++ b/phy/plot/gloo/variable.py @@ -63,12 +63,10 @@ import numpy as np from . import gl -from .globject import GLObject from .array import VertexArray from .buffer import VertexBuffer -from .texture import TextureCube -from .texture import Texture1D, Texture2D - +from .globject import GLObject +from .texture import Texture1D, Texture2D, TextureCube log = logging.getLogger(__name__) @@ -92,25 +90,33 @@ gl.GL_FLOAT_MAT4: (16, gl.GL_FLOAT, np.float32), gl.GL_SAMPLER_1D: (1, gl.GL_UNSIGNED_INT, np.uint32), gl.GL_SAMPLER_2D: (1, gl.GL_UNSIGNED_INT, np.uint32), - gl.GL_SAMPLER_CUBE: (1, gl.GL_UNSIGNED_INT, np.uint32) + gl.GL_SAMPLER_CUBE: (1, gl.GL_UNSIGNED_INT, np.uint32), } # ---------------------------------------------------------- Variable class --- class Variable(GLObject): - """ A variable is an interface between a program and data """ + """A variable is an interface between a program and data""" def __init__(self, program, name, gtype): - """ Initialize the data into default state """ + """Initialize the data into default state""" # Make sure variable type is allowed (for ES 2.0 shader) - if gtype not in [gl.GL_FLOAT, gl.GL_FLOAT_VEC2, - gl.GL_FLOAT_VEC3, gl.GL_FLOAT_VEC4, - gl.GL_INT, gl.GL_BOOL, - gl.GL_FLOAT_MAT2, gl.GL_FLOAT_MAT3, - gl.GL_FLOAT_MAT4, gl.GL_SAMPLER_1D, - gl.GL_SAMPLER_2D, gl.GL_SAMPLER_CUBE]: - raise TypeError("Unknown variable type") + if gtype not in [ + gl.GL_FLOAT, + gl.GL_FLOAT_VEC2, + gl.GL_FLOAT_VEC3, + gl.GL_FLOAT_VEC4, + gl.GL_INT, + gl.GL_BOOL, + gl.GL_FLOAT_MAT2, + gl.GL_FLOAT_MAT3, + gl.GL_FLOAT_MAT4, + gl.GL_SAMPLER_1D, + gl.GL_SAMPLER_2D, + gl.GL_SAMPLER_CUBE, + ]: + raise TypeError('Unknown variable type') GLObject.__init__(self) @@ -135,48 +141,48 @@ def __init__(self, program, name, gtype): @property def name(self): - """ Variable name """ + """Variable name""" return self._name @property def program(self): - """ Program this variable belongs to """ + """Program this variable belongs to""" return self._program @property def gtype(self): - """ Type of the underlying variable (as a GL constant) """ + """Type of the underlying variable (as a GL constant)""" return self._gtype @property def dtype(self): - """ Equivalent dtype of the variable """ + """Equivalent dtype of the variable""" return self._dtype @property def active(self): - """ Whether this variable is active in the program """ + """Whether this variable is active in the program""" return self._active @active.setter def active(self, active): - """ Whether this variable is active in the program """ + """Whether this variable is active in the program""" self._active = active @property def data(self): - """ CPU data """ + """CPU data""" return self._data # ----------------------------------------------------------- Uniform class --- class Uniform(Variable): - """ A Uniform represents a program uniform variable. """ + """A Uniform represents a program uniform variable.""" _ufunctions = { gl.GL_FLOAT: gl.glUniform1fv, @@ -190,11 +196,11 @@ class Uniform(Variable): gl.GL_FLOAT_MAT4: gl.glUniformMatrix4fv, gl.GL_SAMPLER_1D: gl.glUniform1i, gl.GL_SAMPLER_2D: gl.glUniform1i, - gl.GL_SAMPLER_CUBE: gl.glUniform1i + gl.GL_SAMPLER_CUBE: gl.glUniform1i, } def __init__(self, program, name, gtype): - """ Initialize the input into default state """ + """Initialize the input into default state""" Variable.__init__(self, program, name, gtype) size, _, dtype = gl_typeinfo[self._gtype] @@ -203,11 +209,10 @@ def __init__(self, program, name, gtype): self._texture_unit = -1 def set_data(self, data): - """ Assign new data to the variable (deferred operation) """ + """Assign new data to the variable (deferred operation)""" # Textures need special handling if self._gtype == gl.GL_SAMPLER_1D: - if isinstance(data, Texture1D): self._data = data @@ -216,7 +221,7 @@ def set_data(self, data): # Automatic texture creation if required else: - data = np.array(data, copy=False) + data = np.asarray(data) # NumPy 2.0 compatible if data.dtype in [np.float16, np.float32, np.float64]: self._data = data.astype(np.float32).view(Texture1D) else: @@ -231,7 +236,7 @@ def set_data(self, data): # Automatic texture creation if required else: - data = np.array(data, copy=False) + data = np.asarray(data) # NumPy 2.0 compatible if data.dtype in [np.float16, np.float32, np.float64]: self._data = data.astype(np.float32).view(Texture2D) else: @@ -245,30 +250,29 @@ def set_data(self, data): # Automatic texture creation if required else: - data = np.array(data, copy=False) + data = np.asarray(data) # NumPy 2.0 compatible if data.dtype in [np.float16, np.float32, np.float64]: self._data = data.astype(np.float32).view(TextureCube) else: self._data = data.view(TextureCube) else: - self._data[...] = np.array(data, copy=False).ravel() + self._data[...] = np.asarray(data).ravel() # NumPy 2.0 compatible self._need_update = True def _activate(self): if self._gtype in (gl.GL_SAMPLER_1D, gl.GL_SAMPLER_2D, gl.GL_SAMPLER_CUBE): if self.data is not None: - log.log(5, "GPU: Active texture is %d" % self._texture_unit) + log.log(5, f'GPU: Active texture is {self._texture_unit}') gl.glActiveTexture(gl.GL_TEXTURE0 + self._texture_unit) if hasattr(self.data, 'activate'): self.data.activate() def _update(self): - # Check active status (mandatory) if not self._active: - raise RuntimeError("Uniform variable is not active") + raise RuntimeError('Uniform variable is not active') # WARNING : Uniform are supposed to keep their value between program # activation/deactivation (from the GL documentation). It has @@ -285,7 +289,7 @@ def _update(self): # Textures (need to get texture count) elif self._gtype in (gl.GL_SAMPLER_1D, gl.GL_SAMPLER_2D, gl.GL_SAMPLER_CUBE): # texture = self.data - log.log(5, "GPU: Activactin texture %d" % self._texture_unit) + log.log(5, f'GPU: Activactin texture {self._texture_unit}') # gl.glActiveTexture(gl.GL_TEXTURE0 + self._unit) # gl.glBindTexture(texture.target, texture.handle) gl.glUniform1i(self._handle, self._texture_unit) @@ -295,25 +299,24 @@ def _update(self): self._ufunction(self._handle, 1, self._data) def _create(self): - """ Create uniform on GPU (get handle) """ + """Create uniform on GPU (get handle)""" - self._handle = gl.glGetUniformLocation( - self._program.handle, self._name) + self._handle = gl.glGetUniformLocation(self._program.handle, self._name) # --------------------------------------------------------- Attribute class --- class Attribute(Variable): - """ An Attribute represents a program attribute variable """ + """An Attribute represents a program attribute variable""" _afunctions = { gl.GL_FLOAT: gl.glVertexAttrib1f, gl.GL_FLOAT_VEC2: gl.glVertexAttrib2f, gl.GL_FLOAT_VEC3: gl.glVertexAttrib3f, - gl.GL_FLOAT_VEC4: gl.glVertexAttrib4f + gl.GL_FLOAT_VEC4: gl.glVertexAttrib4f, } def __init__(self, program, name, gtype): - """ Initialize the input into default state """ + """Initialize the input into default state""" Variable.__init__(self, program, name, gtype) @@ -324,7 +327,7 @@ def __init__(self, program, name, gtype): self._generic = False def set_data(self, data): - """ Assign new data to the variable (deferred operation) """ + """Assign new data to the variable (deferred operation)""" isnumeric = isinstance(data, (float, int)) @@ -334,14 +337,16 @@ def set_data(self, data): # We already have a vertex buffer # HACK: disable reusing the same buffer for now: fails if the data has not the same shape - #elif isinstance(self._data, (VertexBuffer, VertexArray)) and len(self._data) == len(data): + # elif isinstance(self._data, (VertexBuffer, VertexArray)) and len(self._data) == len(data): # self._data[...] = data # Data is a tuple with size <= 4, we assume this designates a generate # vertex attribute. - elif (isnumeric or (isinstance(data, (tuple, list)) and - len(data) in (1, 2, 3, 4) and - isinstance(data[0], (float, int)))): + elif isnumeric or ( + isinstance(data, (tuple, list)) + and len(data) in (1, 2, 3, 4) + and isinstance(data[0], (float, int)) + ): # Let numpy convert the data for us _, _, dtype = gl_typeinfo[self._gtype] self._data = np.array(data).astype(dtype) @@ -354,7 +359,7 @@ def set_data(self, data): # upload it later to GPU memory. else: # lif not isinstance(data, VertexBuffer): name, base, count = self.dtype - data = np.array(data, dtype=base, copy=False) + data = np.asarray(data, dtype=base) # NumPy 2.0 compatible data = data.ravel().view([(name, base, (count,))]) # WARNING : transform data with the right type # data = np.array(data,copy=False) @@ -370,7 +375,8 @@ def _activate(self): offset = ctypes.c_void_p(self.data.offset) gl.glEnableVertexAttribArray(self.handle) gl.glVertexAttribPointer( - self.handle, size, gtype, gl.GL_FALSE, stride, offset) + self.handle, size, gtype, gl.GL_FALSE, stride, offset + ) def _deactivate(self): if isinstance(self.data, VertexBuffer): @@ -381,21 +387,21 @@ def _deactivate(self): self.data.deactivate() def _update(self): - """ Actual upload of data to GPU memory """ + """Actual upload of data to GPU memory""" - log.log(5, "GPU: Updating %s" % self.name) + log.log(5, f'GPU: Updating {self.name}') if self.data is None or self.data.size == 0: - log.debug("Data is empty for %s" % self.name) + log.debug(f'Data is empty for {self.name}') return else: - log.log(5, "data shape is %s" % self.data.shape) + log.log(5, f'data shape is {self.data.shape}') # Check active status (mandatory) -# if not self._active: -# raise RuntimeError("Attribute variable is not active") -# if self._data is None: -# raise RuntimeError("Attribute variable data is not set") + # if not self._active: + # raise RuntimeError("Attribute variable is not active") + # if self._data is None: + # raise RuntimeError("Attribute variable data is not set") # Generic vertex attribute (all vertices receive the same value) if self._generic: @@ -406,13 +412,13 @@ def _update(self): # Regular vertex buffer elif self.handle >= 0: if self.data is None: - log.warning("data %s is None" % self.name) + log.warning(f'data {self.name} is None') return elif self.data.size == 0: - log.warning("data %s is empty, %s" % (self.name, self.data.shape)) + log.warning(f'data {self.name} is empty, {self.data.shape}') return else: - log.log(5, "data %s is okay %s" % (self.name, self.data.shape)) + log.log(5, f'data {self.name} is okay {self.data.shape}') # Get relevant information from gl_typeinfo size, gtype, dtype = gl_typeinfo[self._gtype] @@ -423,23 +429,24 @@ def _update(self): gl.glEnableVertexAttribArray(self.handle) gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.data.handle) gl.glVertexAttribPointer( - self.handle, size, gtype, gl.GL_FALSE, stride, offset) + self.handle, size, gtype, gl.GL_FALSE, stride, offset + ) def _create(self): - """ Create attribute on GPU (get handle) """ + """Create attribute on GPU (get handle)""" self._handle = gl.glGetAttribLocation(self._program.handle, self.name) @property def size(self): - """ Size of the underlying vertex buffer """ + """Size of the underlying vertex buffer""" if self._data is None: return 0 return self._data.size def __len__(self): - """ Length of the underlying vertex buffer """ + """Length of the underlying vertex buffer""" if self._data is None: return 0 diff --git a/phy/plot/interact.py b/phy/plot/interact.py index c44957a97..69a9046da 100644 --- a/phy/plot/interact.py +++ b/phy/plot/interact.py @@ -1,29 +1,28 @@ -# -*- coding: utf-8 -*- - """Common layouts.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import logging -import numpy as np +import numpy as np from phylib.utils import emit -from phylib.utils.geometry import get_non_overlapping_boxes, get_closest_box +from phylib.utils.geometry import get_closest_box, get_non_overlapping_boxes from .base import BaseLayout -from .transform import Scale, Range, Subplot, Clip, NDC +from .transform import NDC, Clip, Range, Scale, Subplot from .utils import _get_texture, _in_polygon from .visuals import LineVisual, PolygonVisual logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Grid -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + class Grid(BaseLayout): """Layout showing subplots arranged in a 2D grid. @@ -48,13 +47,13 @@ class Grid(BaseLayout): """ - margin = .075 + margin = 0.075 n_dims = 2 active_box = (0, 0) - _scaling = (1., 1.) + _scaling = (1.0, 1.0) def __init__(self, shape=(1, 1), shape_var='u_grid_shape', box_var=None, has_clip=True): - super(Grid, self).__init__(box_var=box_var) + super().__init__(box_var=box_var) self.shape_var = shape_var self._shape = shape ms = 1 - self.margin @@ -69,22 +68,29 @@ def __init__(self, shape=(1, 1), shape_var='u_grid_shape', box_var=None, has_cli if has_clip: self.gpu_transforms.add(Clip([-mc, -mc, +mc, +mc])) # 4. Subplots. - self.gpu_transforms.add(Subplot( - # The parameters of the subplots are callable as they can be changed dynamically. - shape=lambda: self._shape, index=lambda: self.active_box, - shape_gpu_var=self.shape_var, index_gpu_var=self.box_var)) + self.gpu_transforms.add( + Subplot( + # The parameters of the subplots are callable as they can be changed dynamically. + shape=lambda: self._shape, + index=lambda: self.active_box, + shape_gpu_var=self.shape_var, + index_gpu_var=self.box_var, + ) + ) def attach(self, canvas): """Attach the grid to a canvas.""" - super(Grid, self).attach(canvas) + super().attach(canvas) canvas.gpu_transforms += self.gpu_transforms canvas.inserter.insert_vert( - """ - attribute vec2 {}; - uniform vec2 {}; + f""" + attribute vec2 {self.box_var}; + uniform vec2 {self.shape_var}; uniform vec2 u_grid_scaling; - """.format(self.box_var, self.shape_var), - 'header', origin=self) + """, + 'header', + origin=self, + ) def add_boxes(self, canvas, shape=None): """Show subplot boxes.""" @@ -92,13 +98,16 @@ def add_boxes(self, canvas, shape=None): assert isinstance(shape, tuple) n, m = shape n_boxes = n * m - a = 1 - .0001 - - pos = np.array([[-a, -a, +a, -a], - [+a, -a, +a, +a], - [+a, +a, -a, +a], - [-a, +a, -a, -a], - ]) + a = 1 - 0.0001 + + pos = np.array( + [ + [-a, -a, +a, -a], + [+a, -a, +a, +a], + [+a, +a, -a, +a], + [-a, +a, -a, -a], + ] + ) pos = np.tile(pos, (n_boxes, 1)) box_index = [] @@ -120,13 +129,13 @@ def get_closest_box(self, pos): """Get the box index (i, j) closest to a given position in NDC coordinates.""" x, y = pos rows, cols = self.shape - j = np.clip(int(cols * (1. + x) / 2.), 0, cols - 1) - i = np.clip(int(rows * (1. - y) / 2.), 0, rows - 1) + j = np.clip(int(cols * (1.0 + x) / 2.0), 0, cols - 1) + i = np.clip(int(rows * (1.0 - y) / 2.0), 0, rows - 1) return i, j def update_visual(self, visual): """Update a visual.""" - super(Grid, self).update_visual(visual) + super().update_visual(visual) if self.shape_var in visual.program: visual.program[self.shape_var] = self._shape visual.program['u_grid_scaling'] = self._scaling @@ -152,9 +161,10 @@ def scaling(self, value): self.update() -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Boxed -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + class Boxed(BaseLayout): """Layout showing plots in rectangles at arbitrary positions. Used by the waveform view. @@ -180,51 +190,64 @@ class Boxed(BaseLayout): """ - margin = .1 + margin = 0.1 n_dims = 1 active_box = 0 - _box_scaling = (1., 1.) - _layout_scaling = (1., 1.) + _box_scaling = (1.0, 1.0) + _layout_scaling = (1.0, 1.0) _scaling_param_increment = 1.1 def __init__(self, box_pos=None, box_var=None, keep_aspect_ratio=False): - super(Boxed, self).__init__(box_var=box_var) + super().__init__(box_var=box_var) self._key_pressed = None self.keep_aspect_ratio = keep_aspect_ratio self.update_boxes(box_pos) - self.gpu_transforms.add(Range( - NDC, lambda: self.box_bounds[self.active_box], - from_gpu_var='vec4(-1, -1, 1, 1)', to_gpu_var='box_bounds')) + self.gpu_transforms.add( + Range( + NDC, + lambda: self.box_bounds[self.active_box], + from_gpu_var='vec4(-1, -1, 1, 1)', + to_gpu_var='box_bounds', + ) + ) def attach(self, canvas): """Attach the boxed interact to a canvas.""" - super(Boxed, self).attach(canvas) + super().attach(canvas) canvas.gpu_transforms += self.gpu_transforms - canvas.inserter.insert_vert(""" + canvas.inserter.insert_vert( + f""" #include "utils.glsl" - attribute float {}; + attribute float {self.box_var}; uniform sampler2D u_box_pos; uniform float n_boxes; uniform vec2 u_box_size; uniform vec2 u_layout_scaling; - """.format(self.box_var), 'header', origin=self) - canvas.inserter.insert_vert(""" + """, + 'header', + origin=self, + ) + canvas.inserter.insert_vert( + f""" // Fetch the box bounds for the current box (`box_var`). - vec2 box_pos = fetch_texture({}, u_box_pos, n_boxes).xy; + vec2 box_pos = fetch_texture({self.box_var}, u_box_pos, n_boxes).xy; box_pos = (2 * box_pos - 1); // from [0, 1] (texture) to [-1, 1] (NDC) box_pos = box_pos * u_layout_scaling; vec4 box_bounds = vec4(box_pos - u_box_size, box_pos + u_box_size); - """.format(self.box_var), 'start', origin=self) + """, + 'start', + origin=self, + ) def update_visual(self, visual): """Update a visual.""" - super(Boxed, self).update_visual(visual) + super().update_visual(visual) box_pos = _get_texture(self.box_pos, (0, 0), self.n_boxes, [-1, 1]) box_pos = box_pos.astype(np.float32) if 'u_box_pos' in visual.program: - logger.log(5, "Update visual with interact Boxed.") + logger.log(5, 'Update visual with interact Boxed.') visual.program['u_box_pos'] = box_pos visual.program['n_boxes'] = self.n_boxes visual.program['u_box_size'] = np.array(self.box_size) * np.array(self._box_scaling) @@ -237,25 +260,28 @@ def update_boxes(self, box_pos): def add_boxes(self, canvas): """Show the boxes borders.""" n_boxes = len(self.box_pos) - a = 1 + .05 - - pos = np.array([[-a, -a, +a, -a], - [+a, -a, +a, +a], - [+a, +a, -a, +a], - [-a, +a, -a, -a], - ]) + a = 1 + 0.05 + + pos = np.array( + [ + [-a, -a, +a, -a], + [+a, -a, +a, +a], + [+a, +a, -a, +a], + [-a, +a, -a, -a], + ] + ) pos = np.tile(pos, (n_boxes, 1)) boxes = LineVisual() box_index = np.repeat(np.arange(n_boxes), 8) canvas.add_visual(boxes, clearable=False) - boxes.set_data(pos=pos, color=(.5, .5, .5, 1)) + boxes.set_data(pos=pos, color=(0.5, 0.5, 0.5, 1)) boxes.set_box_index(box_index) canvas.update() # Change the box bounds, positions, or size - #-------------------------------------------------------------------------- + # -------------------------------------------------------------------------- @property def n_boxes(self): @@ -273,9 +299,9 @@ def get_closest_box(self, pos): return get_closest_box(pos, self.box_pos, self.box_size) # Box scaling - #-------------------------------------------------------------------------- + # -------------------------------------------------------------------------- - def _increment_box_scaling(self, cw=1., ch=1.): + def _increment_box_scaling(self, cw=1.0, ch=1.0): self._box_scaling = (self._box_scaling[0] * cw, self._box_scaling[1] * ch) self.update() @@ -287,18 +313,18 @@ def expand_box_width(self): return self._increment_box_scaling(cw=self._scaling_param_increment) def shrink_box_width(self): - return self._increment_box_scaling(cw=1. / self._scaling_param_increment) + return self._increment_box_scaling(cw=1.0 / self._scaling_param_increment) def expand_box_height(self): return self._increment_box_scaling(ch=self._scaling_param_increment) def shrink_box_height(self): - return self._increment_box_scaling(ch=1. / self._scaling_param_increment) + return self._increment_box_scaling(ch=1.0 / self._scaling_param_increment) # Layout scaling - #-------------------------------------------------------------------------- + # -------------------------------------------------------------------------- - def _increment_layout_scaling(self, cw=1., ch=1.): + def _increment_layout_scaling(self, cw=1.0, ch=1.0): self._layout_scaling = (self._layout_scaling[0] * cw, self._layout_scaling[1] * ch) self.update() @@ -310,13 +336,13 @@ def expand_layout_width(self): return self._increment_layout_scaling(cw=self._scaling_param_increment) def shrink_layout_width(self): - return self._increment_layout_scaling(cw=1. / self._scaling_param_increment) + return self._increment_layout_scaling(cw=1.0 / self._scaling_param_increment) def expand_layout_height(self): return self._increment_layout_scaling(ch=self._scaling_param_increment) def shrink_layout_height(self): - return self._increment_layout_scaling(ch=1. / self._scaling_param_increment) + return self._increment_layout_scaling(ch=1.0 / self._scaling_param_increment) class Stacked(Boxed): @@ -339,6 +365,7 @@ class Stacked(Boxed): variable specified in `box_var`. """ + margin = 0 _origin = 'bottom' @@ -346,7 +373,7 @@ def __init__(self, n_boxes, box_var=None, origin=None): self._origin = origin or self._origin assert self._origin in ('top', 'bottom') box_pos = self.get_box_pos(n_boxes) - super(Stacked, self).__init__(box_pos, box_var=box_var, keep_aspect_ratio=False) + super().__init__(box_pos, box_var=box_var, keep_aspect_ratio=False) @property def n_boxes(self): @@ -383,18 +410,23 @@ def attach(self, canvas): """Attach the stacked interact to a canvas.""" BaseLayout.attach(self, canvas) canvas.gpu_transforms += self.gpu_transforms - canvas.inserter.insert_vert(""" + canvas.inserter.insert_vert( + f""" #include "utils.glsl" - attribute float {}; + attribute float {self.box_var}; uniform float n_boxes; uniform bool u_top_origin; uniform vec2 u_box_size; - """.format(self.box_var), 'header', origin=self) - canvas.inserter.insert_vert(""" + """, + 'header', + origin=self, + ) + canvas.inserter.insert_vert( + f""" float margin = .1 / n_boxes; float a = 1 - 2. / n_boxes + margin; float b = -1 + 2. / n_boxes - margin; - float u = (u_top_origin ? (n_boxes - 1. - {bv}) : {bv}) / max(1., n_boxes - 1.); + float u = (u_top_origin ? (n_boxes - 1. - {self.box_var}) : {self.box_var}) / max(1., n_boxes - 1.); float y0 = -1 + u * (a + 1); float y1 = b + u * (1 - b); float ym = .5 * (y0 + y1); @@ -402,7 +434,10 @@ def attach(self, canvas): y0 = ym - yh; y1 = ym + yh; vec4 box_bounds = vec4(-1., y0, +1., y1); - """.format(bv=self.box_var), 'before_transforms', origin=self) + """, + 'before_transforms', + origin=self, + ) def update_visual(self, visual): """Update a visual.""" @@ -413,13 +448,15 @@ def update_visual(self, visual): visual.program['u_top_origin'] = self._origin == 'top' -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Interactive tools -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + -class Lasso(object): +class Lasso: """Draw a polygon with the mouse and find the points that belong to the inside of the polygon.""" + def __init__(self): self._points = [] self.canvas = None @@ -430,7 +467,7 @@ def add(self, pos): """Add a point to the polygon.""" x, y = pos.flat if isinstance(pos, np.ndarray) else pos self._points.append((x, y)) - logger.debug("Lasso has %d points.", len(self._points)) + logger.debug('Lasso has %d points.', len(self._points)) self.update_lasso_visual() @property @@ -500,7 +537,7 @@ def on_mouse_click(self, e): if layout: layout.active_box = self.box self.add(pos) # call update_lasso_visual - emit("lasso_updated", self.canvas, self.polygon) + emit('lasso_updated', self.canvas, self.polygon) else: self.clear() self.box = None diff --git a/phy/plot/panzoom.py b/phy/plot/panzoom.py index 975ab65e6..83a803b42 100644 --- a/phy/plot/panzoom.py +++ b/phy/plot/panzoom.py @@ -1,27 +1,25 @@ -# -*- coding: utf-8 -*- - """Pan & zoom transform.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import math import sys import numpy as np - -from .transform import Translate, Scale, pixels_to_ndc +from phylib.utils import connect, emit from phylib.utils._types import _as_array -from phylib.utils import emit, connect +from .transform import Scale, Translate, pixels_to_ndc -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # PanZoom class -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + -class PanZoom(object): +class PanZoom: """Pan and zoom interact. Support mouse and keyboard interactivity. Constructor @@ -88,15 +86,27 @@ class PanZoom(object): _default_zoom_coeff = 1.5 _default_pan = (0, 0) - _default_zoom = 1. - _default_wheel_coeff = .1 + _default_zoom = 1.0 + _default_wheel_coeff = 0.1 _arrows = ('Left', 'Right', 'Up', 'Down') _pm = ('+', '-') def __init__( - self, aspect=None, pan=(0.0, 0.0), zoom=(1.0, 1.0), zmin=1e-5, zmax=1e5, - xmin=None, xmax=None, ymin=None, ymax=None, constrain_bounds=None, - pan_var_name='u_pan', zoom_var_name='u_zoom', enable_mouse_wheel=None): + self, + aspect=None, + pan=(0.0, 0.0), + zoom=(1.0, 1.0), + zmin=1e-5, + zmax=1e5, + xmin=None, + xmax=None, + ymin=None, + ymax=None, + constrain_bounds=None, + pan_var_name='u_pan', + zoom_var_name='u_zoom', + enable_mouse_wheel=None, + ): if constrain_bounds: assert xmin is None assert ymin is None @@ -164,8 +174,7 @@ def xmin(self): @xmin.setter def xmin(self, value): - self._xmin = (np.minimum(value, self._xmax) - if self._xmax is not None else value) + self._xmin = np.minimum(value, self._xmax) if self._xmax is not None else value @property def xmax(self): @@ -174,8 +183,7 @@ def xmax(self): @xmax.setter def xmax(self, value): - self._xmax = (np.maximum(value, self._xmin) - if self._xmin is not None else value) + self._xmax = np.maximum(value, self._xmin) if self._xmin is not None else value # ymin/ymax # ------------------------------------------------------------------------- @@ -187,8 +195,7 @@ def ymin(self): @ymin.setter def ymin(self, value): - self._ymin = (min(value, self._ymax) - if self._ymax is not None else value) + self._ymin = min(value, self._ymax) if self._ymax is not None else value @property def ymax(self): @@ -197,8 +204,7 @@ def ymax(self): @ymax.setter def ymax(self, value): - self._ymax = (max(value, self._ymin) - if self._ymin is not None else value) + self._ymax = max(value, self._ymin) if self._ymin is not None else value # zmin/zmax # ------------------------------------------------------------------------- @@ -227,7 +233,7 @@ def zmax(self, value): def _zoom_aspect(self, zoom=None): zoom = zoom if zoom is not None else self._zoom zoom = _as_array(zoom) - aspect = (self._canvas_aspect * self._aspect if self._aspect is not None else 1.) + aspect = self._canvas_aspect * self._aspect if self._aspect is not None else 1.0 return zoom * aspect def _normalize(self, pos): @@ -236,35 +242,35 @@ def _normalize(self, pos): def _constrain_pan(self): """Constrain bounding box.""" if self.xmin is not None and self.xmax is not None: - p0 = self.xmin + 1. / self._zoom[0] - p1 = self.xmax - 1. / self._zoom[0] + p0 = self.xmin + 1.0 / self._zoom[0] + p1 = self.xmax - 1.0 / self._zoom[0] p0, p1 = min(p0, p1), max(p0, p1) self._pan[0] = np.clip(self._pan[0], p0, p1) if self.ymin is not None and self.ymax is not None: - p0 = self.ymin + 1. / self._zoom[1] - p1 = self.ymax - 1. / self._zoom[1] + p0 = self.ymin + 1.0 / self._zoom[1] + p1 = self.ymax - 1.0 / self._zoom[1] p0, p1 = min(p0, p1), max(p0, p1) self._pan[1] = np.clip(self._pan[1], p0, p1) def _constrain_zoom(self): """Constrain bounding box.""" if self.xmin is not None: - self._zoom[0] = max(self._zoom[0], 1. / (self._pan[0] - self.xmin)) + self._zoom[0] = max(self._zoom[0], 1.0 / (self._pan[0] - self.xmin)) if self.xmax is not None: - self._zoom[0] = max(self._zoom[0], 1. / (self.xmax - self._pan[0])) + self._zoom[0] = max(self._zoom[0], 1.0 / (self.xmax - self._pan[0])) if self.ymin is not None: - self._zoom[1] = max(self._zoom[1], 1. / (self._pan[1] - self.ymin)) + self._zoom[1] = max(self._zoom[1], 1.0 / (self._pan[1] - self.ymin)) if self.ymax is not None: - self._zoom[1] = max(self._zoom[1], 1. / (self.ymax - self._pan[1])) + self._zoom[1] = max(self._zoom[1], 1.0 / (self.ymax - self._pan[1])) def window_to_ndc(self, pos): """Return the mouse coordinates in NDC, taking panzoom into account.""" position = np.asarray(self._normalize(pos)) zoom = np.asarray(self._zoom_aspect()) pan = np.asarray(self.pan) - ndc = ((position / zoom) - pan) + ndc = (position / zoom) - pan return ndc # Pan and zoom @@ -321,7 +327,7 @@ def pan_delta(self, d): self.pan = (pan_x + dx / zoom_x, pan_y + dy / zoom_y) self.update() - def zoom_delta(self, d, p=(0., 0.), c=1.): + def zoom_delta(self, d, p=(0.0, 0.0), c=1.0): """Zoom the view by a given amount.""" dx, dy = d if self.aspect is not None: @@ -335,7 +341,8 @@ def zoom_delta(self, d, p=(0., 0.), c=1.): zoom_x, zoom_y = self._zoom zoom_x_new, zoom_y_new = ( zoom_x * math.exp(c * self._zoom_coeff * dx), - zoom_y * math.exp(c * self._zoom_coeff * dy)) + zoom_y * math.exp(c * self._zoom_coeff * dy), + ) zoom_x_new = max(min(zoom_x_new, self._zmax), self._zmin) zoom_y_new = max(min(zoom_y_new, self._zmax), self._zmin) @@ -347,8 +354,9 @@ def zoom_delta(self, d, p=(0., 0.), c=1.): zoom_x_new, zoom_y_new = self._zoom_aspect((zoom_x_new, zoom_y_new)) self.pan = ( - pan_x - x0 * (1. / zoom_x - 1. / zoom_x_new), - pan_y - y0 * (1. / zoom_y - 1. / zoom_y_new)) + pan_x - x0 * (1.0 / zoom_x - 1.0 / zoom_x_new), + pan_y - y0 * (1.0 / zoom_y - 1.0 / zoom_y_new), + ) self.update() @@ -372,8 +380,8 @@ def set_range(self, bounds, keep_aspect=False): bounds = np.asarray(bounds, dtype=np.float64) v0 = bounds[:2] v1 = bounds[2:] - pan = -.5 * (v0 + v1) - zoom = 2. / (v1 - v0) + pan = -0.5 * (v0 + v1) + zoom = 2.0 / (v1 - v0) if keep_aspect: zoom = zoom.min() * np.ones(2) self.set_pan_zoom(pan=pan, zoom=zoom) @@ -382,8 +390,8 @@ def set_range(self, bounds, keep_aspect=False): def get_range(self): """Return the bounds currently visible.""" p, z = np.asarray(self.pan), np.asarray(self.zoom) - x0, y0 = -1. / z - p - x1, y1 = +1. / z - p + x0, y0 = -1.0 / z - p + x1, y1 = +1.0 / z - p return (x0, y0, x1, y1) def emit_update_events(self): @@ -402,20 +410,20 @@ def emit_update_events(self): def _set_canvas_aspect(self): w, h = self.size - aspect = w / max(float(h), 1.) + aspect = w / max(float(h), 1.0) if aspect > 1.0: self._canvas_aspect = np.array([1.0 / aspect, 1.0]) else: # pragma: no cover self._canvas_aspect = np.array([1.0, aspect / 1.0]) def _zoom_keyboard(self, key): - k = .05 + k = 0.05 if key == '-': k = -k self.zoom_delta((k, k), (0, 0)) def _pan_keyboard(self, key): - k = .1 / np.asarray(self.zoom) + k = 0.1 / np.asarray(self.zoom) if key == 'Left': self.pan_delta((+k[0], +0)) elif key == 'Right': @@ -450,7 +458,7 @@ def on_mouse_move(self, e): if e.button == 'Left': self.pan_delta((dx, dy)) elif e.button == 'Right': - c = np.sqrt(self.size[0]) * .03 + c = np.sqrt(self.size[0]) * 0.03 self.zoom_delta((dx, dy), (x0, y0), c=c) # def on_touch(self, e): @@ -540,12 +548,11 @@ def on_visual_set_data(sender, visual): # Because the visual shaders must be modified to account for u_pan and u_zoom. if not all(v.visual.program is None for v in canvas.visuals): # pragma: no cover - raise RuntimeError("The PanZoom instance must be attached before the visuals.") + raise RuntimeError('The PanZoom instance must be attached before the visuals.') canvas.gpu_transforms.add([self._translate, self._scale], origin=self) # Add the variable declarations. - vs = ('uniform vec2 {};\n'.format(self.pan_var_name) + - 'uniform vec2 {};\n'.format(self.zoom_var_name)) + vs = f'uniform vec2 {self.pan_var_name};\nuniform vec2 {self.zoom_var_name};\n' canvas.inserter.insert_vert(vs, 'header', origin=self) canvas.attach_events(self) diff --git a/phy/plot/plot.py b/phy/plot/plot.py index d6fecd27b..71e8a9b23 100644 --- a/phy/plot/plot.py +++ b/phy/plot/plot.py @@ -1,36 +1,42 @@ -# -*- coding: utf-8 -*- - """Plotting interface.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import logging -import numpy as np import matplotlib as mpl import matplotlib.pyplot as plt +import numpy as np from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar +from phylib.utils._types import _as_tuple from .axes import Axes from .base import BaseCanvas -from .interact import Grid, Boxed, Stacked, Lasso +from .interact import Boxed, Grid, Lasso, Stacked from .panzoom import PanZoom -from .visuals import ( - ScatterVisual, UniformScatterVisual, PlotVisual, UniformPlotVisual, - HistogramVisual, TextVisual, LineVisual, PolygonVisual, - DEFAULT_COLOR) from .transform import NDC -from phylib.utils._types import _as_tuple +from .visuals import ( + DEFAULT_COLOR, + HistogramVisual, + LineVisual, + PlotVisual, + PolygonVisual, + ScatterVisual, + TextVisual, + UniformPlotVisual, + UniformScatterVisual, +) logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Plotting interface -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + class PlotCanvas(BaseCanvas): """Plotting canvas that supports different layouts, subplots, lasso, axes, panzoom.""" @@ -45,7 +51,7 @@ class PlotCanvas(BaseCanvas): _enabled = False def __init__(self, *args, **kwargs): - super(PlotCanvas, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def _enable(self): """Enable panzoom, axes, and lasso if required.""" @@ -58,8 +64,8 @@ def _enable(self): self.enable_lasso() def set_layout( - self, layout=None, shape=None, n_plots=None, origin=None, - box_pos=None, has_clip=True): + self, layout=None, shape=None, n_plots=None, origin=None, box_pos=None, has_clip=True + ): """Set the plot layout: grid, boxed, stacked, or None.""" self.layout = layout @@ -114,7 +120,7 @@ def add_visual(self, visual, *args, **kwargs): self._enable() # The visual is not added again if it has already been added, in which case # the following call is a no-op. - super(PlotCanvas, self).add_visual( + super().add_visual( visual, # Remove special reserved keywords from kwargs, which is otherwise supposed to # contain data for visual.set_data(). @@ -150,7 +156,7 @@ def update_visual(self, visual, *args, **kwargs): return visual # Plot methods - #-------------------------------------------------------------------------- + # -------------------------------------------------------------------------- def scatter(self, *args, **kwargs): """Add a standalone (no batch) scatter plot.""" @@ -158,10 +164,15 @@ def scatter(self, *args, **kwargs): def uscatter(self, *args, **kwargs): """Add a standalone (no batch) uniform scatter plot.""" - return self.add_visual(UniformScatterVisual( - marker=kwargs.pop('marker', None), - color=kwargs.pop('color', None), - size=kwargs.pop('size', None)), *args, **kwargs) + return self.add_visual( + UniformScatterVisual( + marker=kwargs.pop('marker', None), + color=kwargs.pop('color', None), + size=kwargs.pop('size', None), + ), + *args, + **kwargs, + ) def plot(self, *args, **kwargs): """Add a standalone (no batch) plot.""" @@ -188,7 +199,7 @@ def hist(self, *args, **kwargs): return self.add_visual(HistogramVisual(), *args, **kwargs) # Enable methods - #-------------------------------------------------------------------------- + # -------------------------------------------------------------------------- def enable_panzoom(self): """Enable pan zoom in the canvas.""" @@ -206,9 +217,10 @@ def enable_axes(self, data_bounds=None, show_x=True, show_y=True): self.axes.attach(self) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Matplotlib plotting interface -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _zoom_fun(ax, event): # pragma: no cover cur_xlim = ax.get_xlim() @@ -222,11 +234,9 @@ def _zoom_fun(ax, event): # pragma: no cover y_top = ydata - cur_ylim[0] y_bottom = cur_ylim[1] - ydata k = 1.3 - scale_factor = {'up': 1. / k, 'down': k}.get(event.button, 1.) - ax.set_xlim([xdata - x_left * scale_factor, - xdata + x_right * scale_factor]) - ax.set_ylim([ydata - y_top * scale_factor, - ydata + y_bottom * scale_factor]) + scale_factor = {'up': 1.0 / k, 'down': k}.get(event.button, 1.0) + ax.set_xlim([xdata - x_left * scale_factor, xdata + x_right * scale_factor]) + ax.set_ylim([ydata - y_top * scale_factor, ydata + y_bottom * scale_factor]) _MPL_MARKER = { @@ -245,7 +255,7 @@ def _zoom_fun(ax, event): # pragma: no cover } -class PlotCanvasMpl(object): +class PlotCanvasMpl: """Matplotlib backend for a plot canvas (incomplete, work in progress).""" _current_box_index = (0,) @@ -261,7 +271,6 @@ def __init__(self, *args, **kwargs): self.subplots() def set_layout(self, layout=None, shape=None, n_plots=None, origin=None, box_pos=None): - self.layout = layout # Constrain pan zoom. @@ -289,8 +298,7 @@ def subplots(self, nrows=1, ncols=1, **kwargs): return self.axes def iter_ax(self): - for ax in self.axes.flat: - yield ax + yield from self.axes.flat def config_ax(self, ax): xaxis = ax.get_xaxis() @@ -305,7 +313,7 @@ def config_ax(self, ax): yaxis.set_ticks_position('left') yaxis.set_tick_params(direction='out') - ax.grid(color='w', alpha=.2) + ax.grid(color='w', alpha=0.2) def on_zoom(event): # pragma: no cover _zoom_fun(ax, event) @@ -347,8 +355,16 @@ def set_data_bounds(self, data_bounds): self.ax.set_ylim(y0, y1) def scatter( - self, x=None, y=None, pos=None, color=None, - size=None, depth=None, data_bounds=None, marker=None): + self, + x=None, + y=None, + pos=None, + color=None, + size=None, + depth=None, + data_bounds=None, + marker=None, + ): self.ax.scatter(x, y, c=color, s=size, marker=_MPL_MARKER.get(marker, 'o')) self.set_data_bounds(data_bounds) @@ -360,7 +376,7 @@ def hist(self, hist=None, color=None, ylim=None): assert hist is not None n = len(hist) x = np.linspace(-1, 1, n) - self.ax.bar(x, hist, width=2. / (n - 1), color=color) + self.ax.bar(x, hist, width=2.0 / (n - 1), color=color) self.set_data_bounds((-1, 0, +1, ylim)) def lines(self, pos=None, color=None, data_bounds=None): @@ -371,8 +387,7 @@ def lines(self, pos=None, color=None, data_bounds=None): self.ax.plot(x, y, c=color) self.set_data_bounds(data_bounds) - def text(self, pos=None, text=None, anchor=None, - data_bounds=None, color=None): + def text(self, pos=None, text=None, anchor=None, data_bounds=None, color=None): pos = np.atleast_2d(pos) self.ax.text(pos[:, 0], pos[:, 1], text, color=color or 'w') self.set_data_bounds(data_bounds) diff --git a/phy/plot/tests/__init__.py b/phy/plot/tests/__init__.py index 03c2c2c3e..aedafa9c0 100644 --- a/phy/plot/tests/__init__.py +++ b/phy/plot/tests/__init__.py @@ -1,20 +1,20 @@ -from phy.gui.qt import Qt, QPoint, _wait +from phy.gui.qt import QPoint, Qt, _wait def mouse_click(qtbot, c, pos, button='left', modifiers=()): - b = getattr(Qt, button.capitalize() + 'Button') + b = getattr(Qt, f'{button.capitalize()}Button') modifiers = _modifiers_flag(modifiers) qtbot.mouseClick(c, b, modifiers, QPoint(*pos)) def mouse_press(qtbot, c, pos, button='left', modifiers=()): - b = getattr(Qt, button.capitalize() + 'Button') + b = getattr(Qt, f'{button.capitalize()}Button') modifiers = _modifiers_flag(modifiers) qtbot.mousePress(c, b, modifiers, QPoint(*pos)) def mouse_drag(qtbot, c, p0, p1, button='left', modifiers=()): - b = getattr(Qt, button.capitalize() + 'Button') + b = getattr(Qt, f'{button.capitalize()}Button') modifiers = _modifiers_flag(modifiers) qtbot.mousePress(c, b, modifiers, QPoint(*p0)) qtbot.mouseMove(c, QPoint(*p1)) @@ -24,14 +24,14 @@ def mouse_drag(qtbot, c, p0, p1, button='left', modifiers=()): def _modifiers_flag(modifiers): out = Qt.NoModifier for m in modifiers: - out |= getattr(Qt, m + 'Modifier') + out |= getattr(Qt, f'{m}Modifier') return out def key_press(qtbot, c, key, modifiers=(), delay=50): - qtbot.keyPress(c, getattr(Qt, 'Key_' + key), _modifiers_flag(modifiers)) + qtbot.keyPress(c, getattr(Qt, f'Key_{key}'), _modifiers_flag(modifiers)) _wait(delay) def key_release(qtbot, c, key, modifiers=()): - qtbot.keyRelease(c, getattr(Qt, 'Key_' + key), _modifiers_flag(modifiers)) + qtbot.keyRelease(c, getattr(Qt, f'Key_{key}'), _modifiers_flag(modifiers)) diff --git a/phy/plot/tests/conftest.py b/phy/plot/tests/conftest.py index b00c78153..38ff3f22e 100644 --- a/phy/plot/tests/conftest.py +++ b/phy/plot/tests/conftest.py @@ -1,20 +1,18 @@ -# -*- coding: utf-8 -*- - """Test plot.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ from pytest import fixture from ..base import BaseCanvas from ..panzoom import PanZoom - -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Utilities and fixtures -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + @fixture def canvas(qapp, qtbot): diff --git a/phy/plot/tests/test_axes.py b/phy/plot/tests/test_axes.py index ea17522b2..320a1c43d 100644 --- a/phy/plot/tests/test_axes.py +++ b/phy/plot/tests/test_axes.py @@ -1,20 +1,18 @@ -# -*- coding: utf-8 -*- - """Test axes.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import os from ..axes import Axes - -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Tests axes -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_axes_1(qtbot, canvas_pz): c = canvas_pz diff --git a/phy/plot/tests/test_base.py b/phy/plot/tests/test_base.py index 5382aa38b..5ab76eddc 100644 --- a/phy/plot/tests/test_base.py +++ b/phy/plot/tests/test_base.py @@ -1,29 +1,28 @@ -# -*- coding: utf-8 -*- - """Test base.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import logging import numpy as np from pytest import fixture -from ..base import BaseVisual, GLSLInserter, gloo -from ..transform import (subplot_bounds, Translate, Scale, Range, - Clip, Subplot, TransformChain) -from . import mouse_click, mouse_drag, mouse_press, key_press, key_release from phy.gui.qt import QOpenGLWindow +from ..base import BaseVisual, GLSLInserter, gloo +from ..transform import Clip, Range, Scale, Subplot, TransformChain, Translate, subplot_bounds +from . import key_press, key_release, mouse_click, mouse_drag, mouse_press + logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Fixtures -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + @fixture def vertex_shader_nohook(): @@ -57,7 +56,7 @@ def fragment_shader(): class MyVisual(BaseVisual): def __init__(self): - super(MyVisual, self).__init__() + super().__init__() self.set_shader('simple') self.set_primitive_type('lines') @@ -67,9 +66,10 @@ def set_data(self): self.program['u_color'] = [1, 1, 1, 1] -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test base -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_glsl_inserter_nohook(vertex_shader_nohook, fragment_shader): vertex_shader = vertex_shader_nohook @@ -85,7 +85,7 @@ def test_glsl_inserter_hook(vertex_shader, fragment_shader): inserter = GLSLInserter() inserter.insert_vert('uniform float boo;', 'header') inserter.insert_frag('// In fragment shader.', 'before_transforms') - tc = TransformChain([Scale(.5)]) + tc = TransformChain([Scale(0.5)]) inserter.add_gpu_transforms(tc) vs, fs = inserter.insert_into_shaders(vertex_shader, fragment_shader) # assert 'temp_pos_tr = temp_pos_tr * 0.5;' in vs @@ -109,6 +109,7 @@ def test_next_paint(qtbot, canvas): @canvas.on_next_paint def next(): pass + canvas.show() qtbot.waitForWindowShown(canvas) @@ -148,14 +149,13 @@ def test_visual_2(qtbot, canvas, vertex_shader, fragment_shader): class MyVisual2(BaseVisual): def __init__(self): - super(MyVisual2, self).__init__() + super().__init__() self.vertex_shader = vertex_shader self.fragment_shader = fragment_shader self.set_primitive_type('points') - self.transforms.add(Scale((.1, .1))) + self.transforms.add(Scale((0.1, 0.1))) self.transforms.add(Translate((-1, -1))) - self.transforms.add(Range( - (-1, -1, 1, 1), (-1.5, -1.5, 1.5, 1.5))) + self.transforms.add(Range((-1, -1, 1, 1), (-1.5, -1.5, 1.5, 1.5))) s = 'gl_Position.y += (1 + 1e-8 * u_window_size.x);' self.inserter.insert_vert(s, 'after_transforms') self.inserter.add_varying('float', 'v_var', 'gl_Position.x') @@ -198,7 +198,7 @@ def test_visual_benchmark(qtbot, vertex_shader_nohook, fragment_shader): try: from memory_profiler import memory_usage except ImportError: # pragma: no cover - logger.warning("Skip test depending on unavailable memory_profiler module.") + logger.warning('Skip test depending on unavailable memory_profiler module.') return class TestCanvas(QOpenGLWindow): diff --git a/phy/plot/tests/test_interact.py b/phy/plot/tests/test_interact.py index 2c6d97799..399dedbfd 100644 --- a/phy/plot/tests/test_interact.py +++ b/phy/plot/tests/test_interact.py @@ -1,36 +1,33 @@ -# -*- coding: utf-8 -*- - """Test layout.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ from itertools import product import numpy as np -from numpy.testing import assert_equal as ae from numpy.testing import assert_allclose as ac +from numpy.testing import assert_equal as ae -from ..base import BaseVisual, BaseCanvas -from ..interact import Grid, Boxed, Stacked, Lasso +from ..base import BaseCanvas, BaseVisual +from ..interact import Boxed, Grid, Lasso, Stacked from ..panzoom import PanZoom from ..transform import NDC from ..visuals import ScatterVisual from . import mouse_click - -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Fixtures -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ N = 10000 class MyTestVisual(BaseVisual): def __init__(self): - super(MyTestVisual, self).__init__() + super().__init__() self.vertex_shader = """ attribute vec2 a_position; void main() { @@ -71,30 +68,30 @@ def _create_visual(qtbot, canvas, layout, box_index): qtbot.waitForWindowShown(c) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test grid -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_grid_layout(): grid = Grid((4, 8)) - ac(grid.map([0., 0.], (0, 0)), [[-0.875, 0.75]]) - ac(grid.map([0., 0.], (1, 3)), [[-0.125, 0.25]]) - ac(grid.map([0., 0.], (3, 7)), [[0.875, -0.75]]) + ac(grid.map([0.0, 0.0], (0, 0)), [[-0.875, 0.75]]) + ac(grid.map([0.0, 0.0], (1, 3)), [[-0.125, 0.25]]) + ac(grid.map([0.0, 0.0], (3, 7)), [[0.875, -0.75]]) - ac(grid.imap([[0.875, -0.75]], (3, 7)), [[0., 0.]]) + ac(grid.imap([[0.875, -0.75]], (3, 7)), [[0.0, 0.0]]) def test_grid_closest_box(): grid = Grid((3, 7)) - ac(grid.get_closest_box((0., 0.)), (1, 3)) - ac(grid.get_closest_box((-1., +1.)), (0, 0)) - ac(grid.get_closest_box((+1., -1.)), (2, 6)) - ac(grid.get_closest_box((-1., -1.)), (2, 0)) - ac(grid.get_closest_box((+1., +1.)), (0, 6)) + ac(grid.get_closest_box((0.0, 0.0)), (1, 3)) + ac(grid.get_closest_box((-1.0, +1.0)), (0, 0)) + ac(grid.get_closest_box((+1.0, -1.0)), (2, 6)) + ac(grid.get_closest_box((-1.0, -1.0)), (2, 0)) + ac(grid.get_closest_box((+1.0, +1.0)), (0, 6)) def test_grid_1(qtbot, canvas): - n = N // 10 box_index = [[i, j] for i, j in product(range(2), range(5))] @@ -109,7 +106,6 @@ def test_grid_1(qtbot, canvas): def test_grid_2(qtbot, canvas): - n = N // 10 box_index = [[i, j] for i, j in product(range(2), range(5))] @@ -120,21 +116,21 @@ def test_grid_2(qtbot, canvas): grid.shape = (5, 2) assert grid.shape == (5, 2) - grid.scaling = (.5, 2) - assert grid.scaling == (.5, 2) + grid.scaling = (0.5, 2) + assert grid.scaling == (0.5, 2) # qtbot.stop() -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test boxed -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ -def test_boxed_1(qtbot, canvas): +def test_boxed_1(qtbot, canvas): n = 10 b = np.zeros((n, 2)) - b[:, 1] = np.linspace(-1., 1., n) + b[:, 1] = np.linspace(-1.0, 1.0, n) box_index = np.repeat(np.arange(n), N // n, axis=0) assert box_index.shape == (N,) @@ -147,8 +143,8 @@ def test_boxed_1(qtbot, canvas): assert boxed.layout_scaling == (1, 1) ac(boxed.box_pos[:, 0], 0, atol=1e-9) - assert boxed.box_size[0] >= .9 - assert boxed.box_size[1] >= .05 + assert boxed.box_size[0] >= 0.9 + assert boxed.box_size[1] >= 0.05 assert boxed.box_bounds.shape == (n, 4) @@ -169,7 +165,7 @@ def test_boxed_2(qtbot, canvas): n = 10 b = np.zeros((n, 2)) - b[:, 1] = np.linspace(-1., 1., n) + b[:, 1] = np.linspace(-1.0, 1.0, n) box_index = np.repeat(np.arange(n), 2 * (N + 2), axis=0) @@ -180,7 +176,7 @@ def test_boxed_2(qtbot, canvas): t = np.linspace(-1, 1, N) x = np.atleast_2d(t) - y = np.atleast_2d(.5 * np.sin(20 * t)) + y = np.atleast_2d(0.5 * np.sin(20 * t)) x = np.tile(x, (n, 1)) y = np.tile(y, (n, 1)) @@ -194,12 +190,12 @@ def test_boxed_2(qtbot, canvas): qtbot.waitForWindowShown(c) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test stacked -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ -def test_stacked_1(qtbot, canvas): +def test_stacked_1(qtbot, canvas): n = 10 box_index = np.repeat(np.arange(n), N // n, axis=0) @@ -216,25 +212,26 @@ def test_stacked_1(qtbot, canvas): def test_stacked_closest_box(): stacked = Stacked(n_boxes=4, origin='top') - ac(stacked.get_closest_box((-.5, .9)), 0) - ac(stacked.get_closest_box((+.5, -.9)), 3) + ac(stacked.get_closest_box((-0.5, 0.9)), 0) + ac(stacked.get_closest_box((+0.5, -0.9)), 3) stacked = Stacked(n_boxes=4, origin='bottom') - ac(stacked.get_closest_box((-.5, .9)), 3) - ac(stacked.get_closest_box((+.5, -.9)), 0) + ac(stacked.get_closest_box((-0.5, 0.9)), 3) + ac(stacked.get_closest_box((+0.5, -0.9)), 0) stacked.n_boxes = 3 -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test lasso -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_lasso_simple(qtbot): view = BaseCanvas() - x = .25 * np.random.randn(N) - y = .25 * np.random.randn(N) + x = 0.25 * np.random.randn(N) + y = 0.25 * np.random.randn(N) scatter = ScatterVisual() view.add_visual(scatter) @@ -245,15 +242,15 @@ def test_lasso_simple(qtbot): l.create_lasso_visual() view.show() - #qtbot.waitForWindowShown(view) + # qtbot.waitForWindowShown(view) - l.add((-.5, -.5)) - l.add((+.5, -.5)) - l.add((+.5, +.5)) - l.add((-.5, +.5)) + l.add((-0.5, -0.5)) + l.add((+0.5, -0.5)) + l.add((+0.5, +0.5)) + l.add((-0.5, +0.5)) assert l.count == 4 assert l.polygon.shape == (4, 2) - b = [[-.5, -.5], [+.5, -.5], [+.5, +.5], [-.5, +.5]] + b = [[-0.5, -0.5], [+0.5, -0.5], [+0.5, +0.5], [-0.5, +0.5]] ae(l.in_polygon(b), [False, False, True, True]) assert str(l) @@ -303,7 +300,7 @@ def _ctrl_click(x, y, button='left'): assert l.box == (0, 1) inlasso = l.in_polygon(visual.data) - assert .001 < inlasso.mean() < .999 + assert 0.001 < inlasso.mean() < 0.999 # Clear box. _ctrl_click(x0, y0, 'right') diff --git a/phy/plot/tests/test_panzoom.py b/phy/plot/tests/test_panzoom.py index 56c909c0b..6456d99cb 100644 --- a/phy/plot/tests/test_panzoom.py +++ b/phy/plot/tests/test_panzoom.py @@ -1,29 +1,27 @@ -# -*- coding: utf-8 -*- - """Test panzoom.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import os from numpy.testing import assert_allclose as ac from pytest import fixture -from . import mouse_drag, key_press from ..base import BaseVisual from ..panzoom import PanZoom +from . import key_press, mouse_drag - -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Fixtures -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + class MyTestVisual(BaseVisual): def __init__(self): - super(MyTestVisual, self).__init__() + super().__init__() self.set_shader('simple') self.set_primitive_type('lines') @@ -51,23 +49,24 @@ def panzoom(qtbot, canvas_pz): c.close() -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test panzoom -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_panzoom_basic_attrs(): pz = PanZoom() # Aspect. assert pz.aspect is None - pz.aspect = 2. - assert pz.aspect == 2. + pz.aspect = 2.0 + assert pz.aspect == 2.0 # Constraints. for name in ('xmin', 'xmax', 'ymin', 'ymax'): assert getattr(pz, name) is None - setattr(pz, name, 1.) - assert getattr(pz, name) == 1. + setattr(pz, name, 1.0) + assert getattr(pz, name) == 1.0 for name, v in (('zmin', 1e-5), ('zmax', 1e5)): assert getattr(pz, name) == v @@ -81,8 +80,8 @@ def test_panzoom_basic_constrain(): # Aspect. assert pz.aspect is None - pz.aspect = 2. - assert pz.aspect == 2. + pz.aspect = 2.0 + assert pz.aspect == 2.0 # Constraints. assert pz.xmin == pz.ymin == -1 @@ -93,41 +92,41 @@ def test_panzoom_basic_pan_zoom(): pz = PanZoom() # Pan. - assert pz.pan == [0., 0.] - pz.pan = (1., -1.) - assert pz.pan == [1., -1.] + assert pz.pan == [0.0, 0.0] + pz.pan = (1.0, -1.0) + assert pz.pan == [1.0, -1.0] # Zoom. - assert pz.zoom == [1., 1.] - pz.zoom = (2., .5) - assert pz.zoom == [2., .5] - pz.zoom = (1., 1.) + assert pz.zoom == [1.0, 1.0] + pz.zoom = (2.0, 0.5) + assert pz.zoom == [2.0, 0.5] + pz.zoom = (1.0, 1.0) # Pan delta. - pz.pan_delta((-1., 1.)) - assert pz.pan == [0., 0.] + pz.pan_delta((-1.0, 1.0)) + assert pz.pan == [0.0, 0.0] # Zoom delta. - pz.zoom_delta((1., 1.)) + pz.zoom_delta((1.0, 1.0)) assert pz.zoom[0] > 2 assert pz.zoom[0] == pz.zoom[1] - pz.zoom = (1., 1.) + pz.zoom = (1.0, 1.0) # Zoom delta. - pz.zoom_delta((2., 3.), (.5, .5)) + pz.zoom_delta((2.0, 3.0), (0.5, 0.5)) assert pz.zoom[0] > 2 assert pz.zoom[1] > 3 * pz.zoom[0] def test_panzoom_map(): pz = PanZoom() - pz.pan = (1., -1.) - ac(pz.map([0., 0.]), [[1., -1.]]) + pz.pan = (1.0, -1.0) + ac(pz.map([0.0, 0.0]), [[1.0, -1.0]]) - pz.zoom = (2., .5) - ac(pz.map([0., 0.]), [[2., -.5]]) + pz.zoom = (2.0, 0.5) + ac(pz.map([0.0, 0.0]), [[2.0, -0.5]]) - ac(pz.imap([2., -.5]), [[0., 0.]]) + ac(pz.imap([2.0, -0.5]), [[0.0, 0.0]]) def test_panzoom_constraints_x(): @@ -142,8 +141,8 @@ def test_panzoom_constraints_x(): # Zoom beyond the bounds. pz.zoom_delta((-1, -2)) assert pz.pan == [0, 0] - assert pz.zoom[0] == .5 - assert pz.zoom[1] < .5 + assert pz.zoom[0] == 0.5 + assert pz.zoom[1] < 0.5 def test_panzoom_constraints_y(): @@ -158,17 +157,17 @@ def test_panzoom_constraints_y(): # Zoom beyond the bounds. pz.zoom_delta((-2, -1)) assert pz.pan == [0, 0] - assert pz.zoom[0] < .5 - assert pz.zoom[1] == .5 + assert pz.zoom[0] < 0.5 + assert pz.zoom[1] == 0.5 def test_panzoom_constraints_z(): pz = PanZoom() - pz.zmin, pz.zmax = .5, 2 + pz.zmin, pz.zmax = 0.5, 2 # Zoom beyond the bounds. pz.zoom_delta((-10, -10)) - assert pz.zoom == [.5, .5] + assert pz.zoom == [0.5, 0.5] pz.reset() pz.zoom_delta((10, 10)) @@ -185,7 +184,7 @@ def _test_range(*bounds): _test_range(-1, -1, 1, 1) ac(pz.zoom, (1, 1)) - _test_range(-.5, -.5, .5, .5) + _test_range(-0.5, -0.5, 0.5, 0.5) ac(pz.zoom, (2, 2)) _test_range(0, 0, 1, 1) @@ -200,14 +199,15 @@ def _test_range(*bounds): def test_panzoom_mouse_pos(): pz = PanZoom() - pz.zoom_delta((10, 10), (.5, .25)) - pos = pz.window_to_ndc((.01, -.01)) - ac(pos, (.5, .25), atol=1e-3) + pz.zoom_delta((10, 10), (0.5, 0.25)) + pos = pz.window_to_ndc((0.01, -0.01)) + ac(pos, (0.5, 0.25), atol=1e-3) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test panzoom on canvas -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_panzoom_pan_mouse(qtbot, canvas_pz, panzoom): c = canvas_pz diff --git a/phy/plot/tests/test_plot.py b/phy/plot/tests/test_plot.py index c865457e6..0eb51b947 100644 --- a/phy/plot/tests/test_plot.py +++ b/phy/plot/tests/test_plot.py @@ -1,11 +1,9 @@ -# -*- coding: utf-8 -*- - """Test plotting interface.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import os @@ -13,28 +11,30 @@ from pytest import fixture from phy.gui import GUI + from ..plot import PlotCanvas, PlotCanvasMpl from ..utils import get_linear_x from ..visuals import PlotVisual, TextVisual - -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Fixtures -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + @fixture def x(): - return .25 * np.random.randn(1000) + return 0.25 * np.random.randn(1000) @fixture def y(): - return .25 * np.random.randn(1000) + return 0.25 * np.random.randn(1000) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test plotting interface -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + @fixture(params=[True, False]) def canvas(request, qtbot): @@ -54,7 +54,7 @@ def test_plot_0(qtbot, x, y): c.scatter(x=x, y=y) c.show() qtbot.waitForWindowShown(c.canvas) - #c._enable() + # c._enable() c.close() @@ -71,16 +71,16 @@ def test_plot_grid(canvas, x, y): c[0, 0].plot(x=x, y=y) c[0, 1].hist(5 + x[::10]) - c[0, 2].scatter(x, y, color=np.random.uniform(.5, .8, size=(1000, 4))) + c[0, 2].scatter(x, y, color=np.random.uniform(0.5, 0.8, size=(1000, 4))) - c[1, 0].lines(pos=[-1, -.5, +1, -.5]) - c[1, 1].text(pos=(0, 0), text='Hello world!', anchor=(0., 0.)) + c[1, 0].lines(pos=[-1, -0.5, +1, -0.5]) + c[1, 1].text(pos=(0, 0), text='Hello world!', anchor=(0.0, 0.0)) c[1, 1].polygon(pos=np.random.rand(5, 2)) # Multiple scatters in the same subplot. - c[1, 2].scatter(x[2::6], y[2::6], color=(0, 1, 0, .25), size=20, marker='asterisk') - c[1, 2].scatter(x[::5], y[::5], color=(1, 0, 0, .35), size=50, marker='heart') - c[1, 2].scatter(x[1::3], y[1::3], color=(1, 0, 1, .35), size=30, marker='heart') + c[1, 2].scatter(x[2::6], y[2::6], color=(0, 1, 0, 0.25), size=20, marker='asterisk') + c[1, 2].scatter(x[::5], y[::5], color=(1, 0, 0, 0.35), size=50, marker='heart') + c[1, 2].scatter(x[1::3], y[1::3], color=(1, 0, 1, 0.35), size=30, marker='heart') def test_plot_stacked(qtbot, canvas): @@ -93,7 +93,7 @@ def test_plot_stacked(qtbot, canvas): t = get_linear_x(1, 1000).ravel() c[0].scatter(pos=np.random.rand(100, 2)) - c[1].hist(np.random.rand(5, 10), color=np.random.uniform(.4, .9, size=(5, 4))) + c[1].hist(np.random.rand(5, 10), color=np.random.uniform(0.4, 0.9, size=(5, 4))) c[2].plot(t, np.sin(20 * t), color=(1, 0, 0, 1)) @@ -106,21 +106,21 @@ def test_plot_boxed(qtbot, canvas): n = 3 b = np.zeros((n, 2)) - b[:, 0] = np.linspace(-1., 1., n) - b[:, 1] = np.linspace(-1., 1., n) + b[:, 0] = np.linspace(-1.0, 1.0, n) + b[:, 1] = np.linspace(-1.0, 1.0, n) c.set_layout('boxed', box_pos=b) t = get_linear_x(1, 1000).ravel() c[0].scatter(pos=np.random.rand(100, 2)) c[1].plot(t, np.sin(20 * t), color=(1, 0, 0, 1)) - c[2].hist(np.random.rand(5, 10), color=np.random.uniform(.4, .9, size=(5, 4))) + c[2].hist(np.random.rand(5, 10), color=np.random.uniform(0.4, 0.9, size=(5, 4))) def test_plot_uplot(qtbot, canvas): if isinstance(canvas, PlotCanvasMpl): # TODO: not implemented yet return - x, y = .25 * np.random.randn(2, 1000) + x, y = 0.25 * np.random.randn(2, 1000) canvas.uplot(x=x, y=y) @@ -128,7 +128,7 @@ def test_plot_uscatter(qtbot, canvas): if isinstance(canvas, PlotCanvasMpl): # TODO: not implemented yet return - x, y = .25 * np.random.randn(2, 1000) + x, y = 0.25 * np.random.randn(2, 1000) canvas.uscatter(x=x, y=y) @@ -176,19 +176,26 @@ def test_plot_batch_3(qtbot, canvas): canvas.add_visual(visual) visual.add_batch_data( - pos=np.zeros((5, 2)), text=["a" * (i + 1) for i in range(5)], - data_bounds=None, box_index=(0, 0)) + pos=np.zeros((5, 2)), + text=['a' * (i + 1) for i in range(5)], + data_bounds=None, + box_index=(0, 0), + ) visual.add_batch_data( - pos=np.zeros((7, 2)), text=["a" * (i + 1) for i in range(7)], - data_bounds=(-1, -1, 1, 1), box_index=(0, 1)) + pos=np.zeros((7, 2)), + text=['a' * (i + 1) for i in range(7)], + data_bounds=(-1, -1, 1, 1), + box_index=(0, 1), + ) canvas.update_visual(visual) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test matplotlib plotting -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_plot_mpl_1(qtbot): gui = GUI() diff --git a/phy/plot/tests/test_transform.py b/phy/plot/tests/test_transform.py index 9dccf6818..ab74450e6 100644 --- a/phy/plot/tests/test_transform.py +++ b/phy/plot/tests/test_transform.py @@ -1,27 +1,35 @@ -# -*- coding: utf-8 -*- - """Test transform.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ from textwrap import dedent import numpy as np -from numpy.testing import assert_equal as ae from numpy.testing import assert_allclose as ac +from numpy.testing import assert_equal as ae from pytest import fixture from ..transform import ( - _glslify, pixels_to_ndc, _normalize, extend_bounds, - Translate, Scale, Rotate, Range, Clip, Subplot, TransformChain) - - -#------------------------------------------------------------------------------ + Clip, + Range, + Rotate, + Scale, + Subplot, + TransformChain, + Translate, + _glslify, + _normalize, + extend_bounds, + pixels_to_ndc, +) + +# ------------------------------------------------------------------------------ # Fixtures -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _check_forward(transform, array, expected): transformed = transform.apply(array) @@ -46,14 +54,15 @@ def _check(transform, array, expected): _check_forward(inv, expected, array) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test utils -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_glslify(): assert _glslify('a') == 'a', 'b' assert _glslify((1, 2, 3, 4)) == 'vec4(1, 2, 3, 4)' - assert _glslify((1., 2.)) == 'vec2(1.0, 2.0)' + assert _glslify((1.0, 2.0)) == 'vec2(1.0, 2.0)' def test_pixels_to_ndc(): @@ -61,9 +70,9 @@ def test_pixels_to_ndc(): def test_normalize(): - m, M = 0., 10. - arr = np.linspace(0., 10., 10) - ac(_normalize(arr, m, M), np.linspace(-1., 1., 10)) + m, M = 0.0, 10.0 + arr = np.linspace(0.0, 10.0, 10) + ac(_normalize(arr, m, M), np.linspace(-1.0, 1.0, 10)) ac(_normalize(arr, m, m), arr) @@ -72,16 +81,16 @@ def test_extend_bounds(): assert extend_bounds([(0, 0, 0, 0)]) == (-1, -1, 1, 1) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test transform -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_types(): _check(Translate([1, 2]), [], []) - for ab in [[3, 4], [3., 4.]]: - for arr in [ab, [ab], np.array(ab), np.array([ab]), - np.array([ab, ab, ab])]: + for ab in [[3, 4], [3.0, 4.0]]: + for arr in [ab, [ab], np.array(ab), np.array([ab]), np.array([ab, ab, ab])]: _check(Translate([1, 2]), arr, [[4, 6]]) @@ -112,13 +121,12 @@ def test_range_cpu(): _check(Range([0, 0, 1, 1], [-1, -1, 1, 1]), [0.5, 0.5], [[0, 0]]) _check(Range([0, 0, 1, 1], [-1, -1, 1, 1]), [1, 1], [[1, 1]]) - _check(Range([0, 0, 1, 1], [-1, -1, 1, 1]), - [[0, .5], [1.5, -.5]], [[-1, 0], [2, -2]]) + _check(Range([0, 0, 1, 1], [-1, -1, 1, 1]), [[0, 0.5], [1.5, -0.5]], [[-1, 0], [2, -2]]) def test_range_cpu_vectorized(): - arr = np.arange(6).reshape((3, 2)) * 1. - arr_tr = arr / 5. + arr = np.arange(6).reshape((3, 2)) * 1.0 + arr_tr = arr / 5.0 arr_tr[2, :] /= 10 f = np.tile([0, 0, 5, 5], (3, 1)) @@ -145,17 +153,18 @@ def test_subplot_cpu(): shape = (2, 3) _check(Subplot(shape, (0, 0)), [-1, -1], [-1, +0]) - _check(Subplot(shape, (0, 0)), [+0, +0], [-2. / 3., .5]) + _check(Subplot(shape, (0, 0)), [+0, +0], [-2.0 / 3.0, 0.5]) _check(Subplot(shape, (1, 0)), [-1, -1], [-1, -1]) - _check(Subplot(shape, (1, 0)), [+1, +1], [-1. / 3, 0]) + _check(Subplot(shape, (1, 0)), [+1, +1], [-1.0 / 3, 0]) _check(Subplot(shape, (1, 1)), [0, 1], [0, 0]) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test GLSL transforms -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_translate_glsl(): assert 'x = x + u_translate' in Translate(gpu_var='u_translate').glsl('x') @@ -173,7 +182,6 @@ def test_rotate_glsl(): def test_range_glsl(): - assert Range([-1, -1, 1, 1]).glsl('x') r = Range('u_from', 'u_to') assert 'x = (x - ' in r.glsl('x') @@ -197,13 +205,14 @@ def test_subplot_glsl(): assert 'x = ' in glsl -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test transform chain -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + @fixture def array(): - return np.array([[-1., 0.], [1., 2.]]) + return np.array([[-1.0, 0.0], [1.0, 2.0]]) def test_transform_chain_empty(array): @@ -226,7 +235,7 @@ def test_transform_chain_one(array): def test_transform_chain_two(array): translate = Translate([1, 2]) - scale = Scale([.5, .5]) + scale = Scale([0.5, 0.5]) t = TransformChain([translate, scale]) assert t.transforms == [translate, scale] @@ -240,26 +249,26 @@ def test_transform_chain_two(array): def test_transform_chain_complete(array): - t = Scale(.5) + Scale(2.) + Range([-3, -3, 1, 1]) + Subplot('u_shape', 'a_box_index') + t = Scale(0.5) + Scale(2.0) + Range([-3, -3, 1, 1]) + Subplot('u_shape', 'a_box_index') assert len(t.transforms) == 4 - ae(t.apply(array), [[0, .5], [1, 1.5]]) + ae(t.apply(array), [[0, 0.5], [1, 1.5]]) def test_transform_chain_add(): tc = TransformChain() - tc.add([Scale(.5)]) + tc.add([Scale(0.5)]) tc_2 = TransformChain() - tc_2.add([Scale(2.)]) + tc_2.add([Scale(2.0)]) - ae((tc + tc_2).apply([3.]), [[3.]]) + ae((tc + tc_2).apply([3.0]), [[3.0]]) assert str(tc) def test_transform_chain_inverse(): tc = TransformChain() - tc.add([Scale(.5), Translate((1, 0)), Scale(2)]) + tc.add([Scale(0.5), Translate((1, 0)), Scale(2)]) tci = tc.inverse() - ae(tc.apply([[1., 0.]]), [[3., 0.]]) - ae(tci.apply([[3., 0.]]), [[1., 0.]]) + ae(tc.apply([[1.0, 0.0]]), [[3.0, 0.0]]) + ae(tci.apply([[3.0, 0.0]]), [[1.0, 0.0]]) diff --git a/phy/plot/tests/test_utils.py b/phy/plot/tests/test_utils.py index 0c83900b5..6601bea17 100644 --- a/phy/plot/tests/test_utils.py +++ b/phy/plot/tests/test_utils.py @@ -1,25 +1,21 @@ -# -*- coding: utf-8 -*- - """Test plotting utilities.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # 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 raises -from ..utils import ( - _load_shader, _tesselate_histogram, BatchAccumulator, _in_polygon -) +from ..utils import BatchAccumulator, _in_polygon, _load_shader, _tesselate_histogram - -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test utilities -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_load_shader(): assert 'main()' in _load_shader('simple.vert') @@ -53,9 +49,8 @@ def test_accumulator(): def test_in_polygon(): polygon = [[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]] points = np.random.uniform(size=(100, 2), low=-1, high=1) - idx_expected = np.nonzero((points[:, 0] > 0) & - (points[:, 1] > 0) & - (points[:, 0] < 1) & - (points[:, 1] < 1))[0] + idx_expected = np.nonzero( + (points[:, 0] > 0) & (points[:, 1] > 0) & (points[:, 0] < 1) & (points[:, 1] < 1) + )[0] idx = np.nonzero(_in_polygon(points, polygon))[0] ae(idx, idx_expected) diff --git a/phy/plot/tests/test_visuals.py b/phy/plot/tests/test_visuals.py index 49a2a3186..61cb8b807 100644 --- a/phy/plot/tests/test_visuals.py +++ b/phy/plot/tests/test_visuals.py @@ -1,27 +1,36 @@ -# -*- coding: utf-8 -*- - """Test visuals.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import os import numpy as np -from ..visuals import ( - ScatterVisual, PatchVisual, PlotVisual, HistogramVisual, LineVisual, - LineAggGeomVisual, PlotAggVisual, - PolygonVisual, TextVisual, ImageVisual, UniformPlotVisual, UniformScatterVisual) -from ..transform import NDC, Rotate, range_transform from phy.utils.color import _random_color - -#------------------------------------------------------------------------------ +from ..transform import NDC, Rotate, range_transform +from ..visuals import ( + HistogramVisual, + ImageVisual, + LineAggGeomVisual, + LineVisual, + PatchVisual, + PlotAggVisual, + PlotVisual, + PolygonVisual, + ScatterVisual, + TextVisual, + UniformPlotVisual, + UniformScatterVisual, +) + +# ------------------------------------------------------------------------------ # Fixtures -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _test_visual(qtbot, c, v, stop=False, **kwargs): c.add_visual(v) @@ -36,33 +45,32 @@ def _test_visual(qtbot, c, v, stop=False, **kwargs): c.close() -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test scatter visual -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_scatter_empty(qtbot, canvas): _test_visual(qtbot, canvas, ScatterVisual(), x=np.zeros(0), y=np.zeros(0)) def test_scatter_markers(qtbot, canvas_pz): - n = 100 - x = .2 * np.random.randn(n) - y = .2 * np.random.randn(n) + x = 0.2 * np.random.randn(n) + y = 0.2 * np.random.randn(n) _test_visual(qtbot, canvas_pz, ScatterVisual(marker='vbar'), x=x, y=y, data_bounds='auto') def test_scatter_custom(qtbot, canvas_pz): - n = 100 # Random position. - pos = .2 * np.random.randn(n, 2) + pos = 0.2 * np.random.randn(n, 2) # Random colors. - c = np.random.uniform(.4, .7, size=(n, 4)) - c[:, -1] = .5 + c = np.random.uniform(0.4, 0.7, size=(n, 4)) + c[:, -1] = 0.5 # Random sizes s = 5 + 20 * np.random.rand(n) @@ -70,33 +78,32 @@ def test_scatter_custom(qtbot, canvas_pz): _test_visual(qtbot, canvas_pz, ScatterVisual(), pos=pos, color=c, size=s) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test patch visual -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_patch_empty(qtbot, canvas): _test_visual(qtbot, canvas, PatchVisual(), x=np.zeros(0), y=np.zeros(0)) def test_patch_1(qtbot, canvas_pz): - n = 100 - x = .2 * np.random.randn(n) - y = .2 * np.random.randn(n) + x = 0.2 * np.random.randn(n) + y = 0.2 * np.random.randn(n) _test_visual(qtbot, canvas_pz, PatchVisual(), x=x, y=y, data_bounds='auto') def test_patch_2(qtbot, canvas_pz): - n = 100 # Random position. - pos = .2 * np.random.randn(n, 2) + pos = 0.2 * np.random.randn(n, 2) # Random colors. - c = np.random.uniform(.4, .7, size=(n, 4)) - c[:, -1] = .5 + c = np.random.uniform(0.4, 0.7, size=(n, 4)) + c[:, -1] = 0.5 v = PatchVisual(primitive_type='triangles') canvas_pz.add_visual(v) @@ -111,44 +118,52 @@ def test_patch_2(qtbot, canvas_pz): canvas_pz.close() -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test uniform scatter visual -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_uniform_scatter_empty(qtbot, canvas): _test_visual(qtbot, canvas, UniformScatterVisual(), x=np.zeros(0), y=np.zeros(0)) def test_uniform_scatter_markers(qtbot, canvas_pz): - n = 100 - x = .2 * np.random.randn(n) - y = .2 * np.random.randn(n) + x = 0.2 * np.random.randn(n) + y = 0.2 * np.random.randn(n) _test_visual( - qtbot, canvas_pz, UniformScatterVisual(marker='vbar'), x=x, y=y, data_bounds='auto') + qtbot, canvas_pz, UniformScatterVisual(marker='vbar'), x=x, y=y, data_bounds='auto' + ) def test_uniform_scatter_custom(qtbot, canvas_pz): - n = 100 # Random position. - pos = .2 * np.random.randn(n, 2) + pos = 0.2 * np.random.randn(n, 2) _test_visual( - qtbot, canvas_pz, UniformScatterVisual(color=_random_color() + (.5,), size=10., ), - pos=pos, masks=np.linspace(0., 1., n), data_bounds=None) - - -#------------------------------------------------------------------------------ + qtbot, + canvas_pz, + UniformScatterVisual( + color=_random_color() + (0.5,), + size=10.0, + ), + pos=pos, + masks=np.linspace(0.0, 1.0, n), + data_bounds=None, + ) + + +# ------------------------------------------------------------------------------ # Test plot visual -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_plot_empty(qtbot, canvas): y = np.zeros((1, 0)) - _test_visual(qtbot, canvas, PlotVisual(), - y=y) + _test_visual(qtbot, canvas, PlotVisual(), y=y) def test_plot_0(qtbot, canvas_pz): @@ -157,50 +172,51 @@ def test_plot_0(qtbot, canvas_pz): def test_plot_1(qtbot, canvas_pz): - y = .2 * np.random.randn(10) + y = 0.2 * np.random.randn(10) _test_visual(qtbot, canvas_pz, PlotVisual(), y=y, data_bounds='auto') def test_plot_color(qtbot, canvas_pz): v = PlotVisual() canvas_pz.add_visual(v) - data = v.validate(y=.2 * np.random.randn(10), data_bounds='auto') + data = v.validate(y=0.2 * np.random.randn(10), data_bounds='auto') assert v.vertex_count(**data) >= 0 v.set_data(**data) - v.set_color(np.random.uniform(low=.5, high=.9, size=(10, 4))) + v.set_color(np.random.uniform(low=0.5, high=0.9, size=(10, 4))) canvas_pz.show() qtbot.waitForWindowShown(canvas_pz) canvas_pz.close() def test_plot_2(qtbot, canvas_pz): - n_signals = 50 n_samples = 10 y = 20 * np.random.randn(n_signals, n_samples) # Signal colors. - c = np.random.uniform(.5, 1, size=(n_signals, 4)) - c[:, 3] = .5 + c = np.random.uniform(0.5, 1, size=(n_signals, 4)) + c[:, 3] = 0.5 # Depth. - depth = np.linspace(0., -1., n_signals) + depth = np.linspace(0.0, -1.0, n_signals) _test_visual( - qtbot, canvas_pz, PlotVisual(), y=y, depth=depth, data_bounds=[-1, -50, 1, 50], color=c) + qtbot, canvas_pz, PlotVisual(), y=y, depth=depth, data_bounds=[-1, -50, 1, 50], color=c + ) def test_plot_list(qtbot, canvas_pz): - y = [.25 * np.random.randn(i) for i in (5, 20, 50)] + y = [0.25 * np.random.randn(i) for i in (5, 20, 50)] c = [[0, 0, 1, 1], [0, 0, 1, 1], [0, 0, 1, 1]] - masks = [0., 0.5, 1.0] + masks = [0.0, 0.5, 1.0] _test_visual(qtbot, canvas_pz, PlotVisual(), y=y, color=c, masks=masks) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test uniform plot visual -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_uniform_plot_empty(qtbot, canvas): y = np.zeros((1, 0)) @@ -213,24 +229,27 @@ def test_uniform_plot_0(qtbot, canvas_pz): def test_uniform_plot_1(qtbot, canvas_pz): - y = .2 * np.random.randn(10) - _test_visual(qtbot, canvas_pz, UniformPlotVisual(), y=y, masks=.5, data_bounds=NDC) + y = 0.2 * np.random.randn(10) + _test_visual(qtbot, canvas_pz, UniformPlotVisual(), y=y, masks=0.5, data_bounds=NDC) def test_uniform_plot_2(qtbot, canvas_pz): - y = .2 * np.random.randn(10) - _test_visual(qtbot, canvas_pz, UniformPlotVisual(), y=y, masks=.5, data_bounds='auto') + y = 0.2 * np.random.randn(10) + _test_visual(qtbot, canvas_pz, UniformPlotVisual(), y=y, masks=0.5, data_bounds='auto') def test_uniform_plot_list(qtbot, canvas_pz): y = [np.random.randn(i) for i in (5, 20)] - _test_visual(qtbot, canvas_pz, UniformPlotVisual(color=(1., 0., 0., 1.)), y=y, masks=[.1, .9]) + _test_visual( + qtbot, canvas_pz, UniformPlotVisual(color=(1.0, 0.0, 0.0, 1.0)), y=y, masks=[0.1, 0.9] + ) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test histogram visual -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_histogram_empty(qtbot, canvas): hist = np.zeros((1, 0)) @@ -248,16 +267,16 @@ def test_histogram_1(qtbot, canvas_pz): def test_histogram_2(qtbot, canvas_pz): - n_hists = 5 hist = np.random.rand(n_hists, 21) # Histogram colors. - c = np.random.uniform(.3, .6, size=(n_hists, 4)) + c = np.random.uniform(0.3, 0.6, size=(n_hists, 4)) c[:, 3] = 1 _test_visual( - qtbot, canvas_pz, HistogramVisual(), hist=hist, color=c, ylim=2 * np.ones(n_hists)) + qtbot, canvas_pz, HistogramVisual(), hist=hist, color=c, ylim=2 * np.ones(n_hists) + ) def test_histogram_3(qtbot, canvas_pz): @@ -267,9 +286,10 @@ def test_histogram_3(qtbot, canvas_pz): _test_visual(qtbot, canvas_pz, visual, hist=hist) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test image visual -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_image_empty(qtbot, canvas): image = np.zeros((0, 0, 4)) @@ -287,13 +307,14 @@ def test_image_1(qtbot, canvas): def test_image_2(qtbot, canvas): n = 100 _test_visual( - qtbot, canvas, ImageVisual(), - image=np.random.uniform(low=.5, high=.9, size=(n, n, 4))) + qtbot, canvas, ImageVisual(), image=np.random.uniform(low=0.5, high=0.9, size=(n, n, 4)) + ) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test line visual -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_line_empty(qtbot, canvas): pos = np.zeros((0, 4)) @@ -302,72 +323,80 @@ def test_line_empty(qtbot, canvas): def test_line_0(qtbot, canvas_pz): n = 10 - y = np.linspace(-.5, .5, 10) + y = np.linspace(-0.5, 0.5, 10) pos = np.c_[-np.ones(n), y, np.ones(n), y] - color = np.random.uniform(.5, .9, (n, 4)) + color = np.random.uniform(0.5, 0.9, (n, 4)) _test_visual(qtbot, canvas_pz, LineVisual(), pos=pos, color=color, data_bounds=[-1, -1, 1, 1]) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test line agg geom -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_line_agg_geom_0(qtbot, canvas_pz): n = 1024 T = np.linspace(0, 10 * 2 * np.pi, n) - R = np.linspace(0, .5, n) + R = np.linspace(0, 0.5, n) P = np.zeros((n, 2), dtype=np.float64) P[:, 0] = np.cos(T) * R P[:, 1] = np.sin(T) * R P = range_transform([NDC], [[0, 0, 1034, 1034]], P) - color = np.random.uniform(.5, .9, 4) - _test_visual( - qtbot, canvas_pz, LineAggGeomVisual(), pos=P, color=color) + color = np.random.uniform(0.5, 0.9, 4) + _test_visual(qtbot, canvas_pz, LineAggGeomVisual(), pos=P, color=color) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test plot agg -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_plot_agg_empty(qtbot, canvas_pz): - _test_visual( - qtbot, canvas_pz, PlotAggVisual(), y=[]) + _test_visual(qtbot, canvas_pz, PlotAggVisual(), y=[]) def test_plot_agg_1(qtbot, canvas_pz): t = np.linspace(-np.pi, np.pi, 8) t = t[:-1] - x = .5 * np.cos(t) - y = .5 * np.sin(t) + x = 0.5 * np.cos(t) + y = 0.5 * np.sin(t) - _test_visual( - qtbot, canvas_pz, PlotAggVisual(closed=True), x=x, y=y, data_bounds='auto') + _test_visual(qtbot, canvas_pz, PlotAggVisual(closed=True), x=x, y=y, data_bounds='auto') def test_plot_agg_2(qtbot, canvas_pz): n_signals = 100 n_samples = 1000 - x = np.linspace(-1., 1., n_samples) + x = np.linspace(-1.0, 1.0, n_samples) y = np.sin(10 * x) * 0.1 x = np.tile(x, (n_signals, 1)) y = np.tile(y, (n_signals, 1)) y -= np.linspace(-1, 1, n_signals)[:, np.newaxis] - color = np.random.uniform(low=.5, high=.9, size=(n_signals, 4)) + color = np.random.uniform(low=0.5, high=0.9, size=(n_signals, 4)) depth = np.random.uniform(low=0, high=1, size=n_signals) masks = np.random.uniform(low=0, high=1, size=n_signals) _test_visual( - qtbot, canvas_pz, PlotAggVisual(), x=x, y=y, color=color, - depth=depth, masks=masks, data_bounds=NDC) - - -#------------------------------------------------------------------------------ + qtbot, + canvas_pz, + PlotAggVisual(), + x=x, + y=y, + color=color, + depth=depth, + masks=masks, + data_bounds=NDC, + ) + + +# ------------------------------------------------------------------------------ # Test polygon visual -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_polygon_empty(qtbot, canvas): pos = np.zeros((0, 2)) @@ -376,15 +405,16 @@ def test_polygon_empty(qtbot, canvas): def test_polygon_0(qtbot, canvas_pz): n = 9 - x = .5 * np.cos(np.linspace(0., 2 * np.pi, n)) - y = .5 * np.sin(np.linspace(0., 2 * np.pi, n)) + x = 0.5 * np.cos(np.linspace(0.0, 2 * np.pi, n)) + y = 0.5 * np.sin(np.linspace(0.0, 2 * np.pi, n)) pos = np.c_[x, y] _test_visual(qtbot, canvas_pz, PolygonVisual(), pos=pos) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test text visual -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_text_empty(qtbot, canvas): pos = np.zeros((0, 2)) @@ -400,19 +430,18 @@ def test_text_1(qtbot, canvas_pz): text = '0123456789' text = [text[:n] for n in range(1, 11)] - pos = np.c_[np.linspace(-.5, .5, 10), np.linspace(-.5, .5, 10)] + pos = np.c_[np.linspace(-0.5, 0.5, 10), np.linspace(-0.5, 0.5, 10)] color = np.ones((10, 4)) color[:, 2] = 0 - _test_visual( - qtbot, canvas_pz, TextVisual(font_size=32), pos=pos, text=text, color=color) + _test_visual(qtbot, canvas_pz, TextVisual(font_size=32), pos=pos, text=text, color=color) def test_text_2(qtbot, canvas_pz): c = canvas_pz text = ['12345'] * 5 - pos = [[0, 0], [-.5, +.5], [+.5, +.5], [-.5, -.5], [+.5, -.5]] + pos = [[0, 0], [-0.5, +0.5], [+0.5, +0.5], [-0.5, -0.5], [+0.5, -0.5]] anchor = [[0, 0], [-1, +1], [+1, +1], [-1, -1], [+1, -1]] v = TextVisual() @@ -424,7 +453,7 @@ def test_text_2(qtbot, canvas_pz): v.set_data(pos=pos, data_bounds=None) v.set_marker_size(10) - v.set_color(np.random.uniform(low=.5, high=.9, size=(v.n_vertices, 4))) + v.set_color(np.random.uniform(low=0.5, high=0.9, size=(v.n_vertices, 4))) c.show() qtbot.waitForWindowShown(c) @@ -439,5 +468,10 @@ def test_text_3(qtbot, canvas_pz): text = [text] * 10 _test_visual( - qtbot, canvas_pz, TextVisual(color=(1, 1, 0, 1)), pos=[(0, 0)] * 10, text=text, - anchor=[(1, -1 - 2 * i) for i in range(5)] + [(-1 - 2 * i, 1) for i in range(5)]) + qtbot, + canvas_pz, + TextVisual(color=(1, 1, 0, 1)), + pos=[(0, 0)] * 10, + text=text, + anchor=[(1, -1 - 2 * i) for i in range(5)] + [(-1 - 2 * i, 1) for i in range(5)], + ) diff --git a/phy/plot/transform.py b/phy/plot/transform.py index 12fbba674..6653853a0 100644 --- a/phy/plot/transform.py +++ b/phy/plot/transform.py @@ -1,28 +1,27 @@ -# -*- coding: utf-8 -*- - """Transforms.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import logging from textwrap import dedent import numpy as np - from phylib.utils.geometry import range_transform logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Utils -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _wrap_apply(f): """Validate the input and output of transform apply functions.""" + def wrapped(arr, **kwargs): if arr is None or not len(arr): return arr @@ -35,15 +34,18 @@ def wrapped(arr, **kwargs): assert out.ndim == 2 assert out.shape[1] == arr.shape[1] return out + return wrapped def _wrap_glsl(f): """Validate the output of GLSL functions.""" + def wrapped(var, **kwargs): out = f(var, **kwargs) out = dedent(out).strip() return out + return wrapped @@ -54,12 +56,12 @@ def _glslify(r): else: r = _call_if_callable(r) assert 2 <= len(r) <= 4 - return 'vec{}({})'.format(len(r), ', '.join(map(str, r))) + return f'vec{len(r)}({", ".join(map(str, r))})' def _call_if_callable(s): """Call a variable if it's a callable, otherwise return it.""" - if hasattr(s, '__call__'): + if callable(s): return s() return s @@ -74,20 +76,20 @@ def _minus(value): def _inverse(value): if isinstance(value, np.ndarray): - return 1. / value + return 1.0 / value elif hasattr(value, '__len__'): assert len(value) == 2 - return 1. / value[0], 1. / value[1] + return 1.0 / value[0], 1.0 / value[1] else: - return 1. / value + return 1.0 / value def _normalize(arr, m, M): d = float(M - m) if abs(d) < 1e-9: return arr - b = 2. / d - a = -1 - 2. * m / d + b = 2.0 / d + a = -1 - 2.0 * m / d arr *= b arr += a return arr @@ -96,9 +98,7 @@ def _normalize(arr, m, M): def _fix_coordinate_in_visual(visual, coord): """Insert GLSL code to fix the position on the x or y coordinate.""" assert coord in ('x', 'y') - visual.inserter.insert_vert( - 'gl_Position.{coord} = pos_orig.{coord};'.format(coord=coord), - 'after_transforms') + visual.inserter.insert_vert(f'gl_Position.{coord} = pos_orig.{coord};', 'after_transforms') def subplot_bounds(shape=None, index=None): @@ -120,12 +120,12 @@ def subplot_bounds(shape=None, index=None): def subplot_bounds_glsl(shape=None, index=None): """Get the data bounds in GLSL of a subplot.""" - x0 = '-1.0 + 2.0 * {i}.y / {s}.y'.format(s=shape, i=index) - y0 = '+1.0 - 2.0 * ({i}.x + 1) / {s}.x'.format(s=shape, i=index) - x1 = '-1.0 + 2.0 * ({i}.y + 1) / {s}.y'.format(s=shape, i=index) - y1 = '+1.0 - 2.0 * ({i}.x) / {s}.x'.format(s=shape, i=index) + x0 = f'-1.0 + 2.0 * {index}.y / {shape}.y' + y0 = f'+1.0 - 2.0 * ({index}.x + 1) / {shape}.x' + x1 = f'-1.0 + 2.0 * ({index}.y + 1) / {shape}.y' + y1 = f'+1.0 - 2.0 * ({index}.x) / {shape}.x' - return 'vec4(\n{x0}, \n{y0}, \n{x1}, \n{y1})'.format(x0=x0, y0=y0, x1=x1, y1=y1) + return f'vec4(\n{x0}, \n{y0}, \n{x1}, \n{y1})' def extend_bounds(bounds_list): @@ -146,7 +146,7 @@ def pixels_to_ndc(pos, size=None): """Convert from pixels to normalized device coordinates (in [-1, 1]).""" pos = np.asarray(pos, dtype=np.float64) size = np.asarray(size, dtype=np.float64) - pos = pos / (size / 2.) - 1 + pos = pos / (size / 2.0) - 1 # Flip y, because the origin in pixels is at the top left corner of the # window. pos[1] = -pos[1] @@ -157,12 +157,14 @@ def pixels_to_ndc(pos, size=None): NDC = (-1.0, -1.0, +1.0, +1.0) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Base Transform -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + -class BaseTransform(object): +class BaseTransform: """Base class for all transforms.""" + def __init__(self, **kwargs): self.__dict__.update(**{k: v for k, v in kwargs.items() if v is not None}) @@ -186,9 +188,10 @@ def __add__(self, other): return TransformChain().add([self, other]) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Transforms -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + class Translate(BaseTransform): """Translation transform. @@ -206,7 +209,7 @@ class Translate(BaseTransform): gpu_var = None def __init__(self, amount=None, **kwargs): - super(Translate, self).__init__(amount=amount, **kwargs) + super().__init__(amount=amount, **kwargs) def apply(self, arr, param=None): """Apply a translation to a NumPy array.""" @@ -217,16 +220,17 @@ def apply(self, arr, param=None): def glsl(self, var): """Return a GLSL snippet that applies the translation to a given GLSL variable name.""" assert var - return ''' + return f""" // Translate transform. - {var} = {var} + {translate}; - '''.format(var=var, translate=self.gpu_var or _call_if_callable(self.amount)) + {var} = {var} + {self.gpu_var or _call_if_callable(self.amount)}; + """ def inverse(self): """Return the inverse Translate instance.""" return Translate( amount=_minus(_call_if_callable(self.amount)) if self.amount is not None else None, - gpu_var=('-%s' % self.gpu_var) if self.gpu_var else None) + gpu_var=f'-{self.gpu_var}' if self.gpu_var else None, + ) class Scale(BaseTransform): @@ -245,7 +249,7 @@ class Scale(BaseTransform): gpu_var = None def __init__(self, amount=None, **kwargs): - super(Scale, self).__init__(amount=amount, **kwargs) + super().__init__(amount=amount, **kwargs) def apply(self, arr, param=None): """Apply a scaling to a NumPy array.""" @@ -256,16 +260,17 @@ def apply(self, arr, param=None): def glsl(self, var): """Return a GLSL snippet that applies the scaling to a given GLSL variable name.""" assert var - return ''' + return f""" // Translate transform. - {var} = {var} * {scaling}; - '''.format(var=var, scaling=self.gpu_var or _call_if_callable(self.amount)) + {var} = {var} * {self.gpu_var or _call_if_callable(self.amount)}; + """ def inverse(self): """Return the inverse Scale instance.""" return Scale( amount=_inverse(_call_if_callable(self.amount)) if self.amount is not None else None, - gpu_var=('1.0 / %s' % self.gpu_var) if self.gpu_var else None) + gpu_var=f'1.0 / {self.gpu_var}' if self.gpu_var else None, + ) class Rotate(BaseTransform): @@ -281,7 +286,7 @@ class Rotate(BaseTransform): direction = 'cw' def __init__(self, direction=None, **kwargs): - super(Rotate, self).__init__(direction=direction, **kwargs) + super().__init__(direction=direction, **kwargs) def apply(self, arr, direction=None): """Apply a rotation to a NumPy array.""" @@ -303,10 +308,10 @@ def glsl(self, var): direction = self.direction or 'cw' assert direction in ('cw', 'ccw') m = '' if direction == 'ccw' else '-' - return ''' + return f""" // Rotation transform. {var} = {m}vec2(-{var}.y, {var}.x); - '''.format(var=var, m=m) + """ def inverse(self): """Return the inverse Rotate instance.""" @@ -338,7 +343,7 @@ class Range(BaseTransform): to_gpu_var = None def __init__(self, from_bounds=None, to_bounds=None, **kwargs): - super(Range, self).__init__(from_bounds=from_bounds, to_bounds=to_bounds, **kwargs) + super().__init__(from_bounds=from_bounds, to_bounds=to_bounds, **kwargs) def apply(self, arr, from_bounds=None, to_bounds=None): """Apply the transform to a NumPy array.""" @@ -358,19 +363,21 @@ def glsl(self, var): from_bounds = _glslify(self.from_gpu_var or self.from_bounds) to_bounds = _glslify(self.to_gpu_var or self.to_bounds) - return ''' + return f""" // Range transform. - {var} = ({var} - {f}.xy); - {var} = {var} * ({t}.zw - {t}.xy); - {var} = {var} / ({f}.zw - {f}.xy); - {var} = {var} + {t}.xy; - '''.format(var=var, f=from_bounds, t=to_bounds) + {var} = ({var} - {from_bounds}.xy); + {var} = {var} * ({to_bounds}.zw - {to_bounds}.xy); + {var} = {var} / ({from_bounds}.zw - {from_bounds}.xy); + {var} = {var} + {to_bounds}.xy; + """ def inverse(self): """Return the inverse Range instance.""" return Range( - from_bounds=self.to_bounds, to_bounds=self.from_bounds, - from_gpu_var=self.to_gpu_var, to_gpu_var=self.from_gpu_var, + from_bounds=self.to_bounds, + to_bounds=self.from_bounds, + from_gpu_var=self.to_gpu_var, + to_gpu_var=self.from_gpu_var, ) @@ -406,14 +413,17 @@ def Subplot(shape=None, index=None, shape_gpu_var=None, index_gpu_var=None): if shape_gpu_var is not None: to_gpu_var = subplot_bounds_glsl(shape=shape_gpu_var, index=index_gpu_var) if shape is not None: - if hasattr(shape, '__call__') and hasattr(index, '__call__'): + if callable(shape) and callable(index): to_bounds = lambda: subplot_bounds(shape(), index()) else: to_bounds = subplot_bounds(shape, index) return Range( - from_bounds=from_bounds, to_bounds=to_bounds, - from_gpu_var=from_gpu_var, to_gpu_var=to_gpu_var) + from_bounds=from_bounds, + to_bounds=to_bounds, + from_gpu_var=from_gpu_var, + to_gpu_var=to_gpu_var, + ) class Clip(BaseTransform): @@ -430,17 +440,19 @@ class Clip(BaseTransform): bounds = NDC def __init__(self, bounds=None, **kwargs): - super(Clip, self).__init__(bounds=bounds, **kwargs) + super().__init__(bounds=bounds, **kwargs) def apply(self, arr, bounds=None): """Apply the clipping to a NumPy array.""" bounds = bounds if bounds is not None else _call_if_callable(self.bounds) assert isinstance(bounds, (tuple, list)) assert len(bounds) == 4 - index = ((arr[:, 0] >= bounds[0]) & - (arr[:, 1] >= bounds[1]) & - (arr[:, 0] <= bounds[2]) & - (arr[:, 1] <= bounds[3])) + index = ( + (arr[:, 0] >= bounds[0]) + & (arr[:, 1] >= bounds[1]) + & (arr[:, 0] <= bounds[2]) + & (arr[:, 1] <= bounds[3]) + ) return arr[index, ...] def glsl(self, var): @@ -449,7 +461,7 @@ def glsl(self, var): assert var bounds = _glslify(self.bounds) - return """ + return f""" // Clip transform. if (({var}.x < {bounds}.x) || ({var}.y < {bounds}.y) || @@ -457,19 +469,21 @@ def glsl(self, var): ({var}.y > {bounds}.w)) {{ discard; }} - """.format(bounds=bounds, var=var) + """ def inverse(self): """Return the same instance (the inverse has no sense for a Clip transform).""" return self -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Transform chain -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + -class TransformChain(object): +class TransformChain: """A linear sequence of transforms.""" + def __init__(self, transforms=None, origin=None): self.transformed_var_name = None self.origin = origin @@ -507,8 +521,8 @@ def apply(self, arr): def inverse(self): """Return the inverse chain of transforms.""" inv_transforms = [ - (transform.inverse(), origin) - for (transform, origin) in self._transforms[::-1]] + (transform.inverse(), origin) for (transform, origin) in self._transforms[::-1] + ] inv = TransformChain() inv._transforms = inv_transforms return inv diff --git a/phy/plot/utils.py b/phy/plot/utils.py index 6c8885ada..a5341b06e 100644 --- a/phy/plot/utils.py +++ b/phy/plot/utils.py @@ -1,25 +1,23 @@ -# -*- coding: utf-8 -*- - """Plotting utilities.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import logging from pathlib import Path import numpy as np - from phylib.utils import Bunch, _as_array logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Data validation -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _get_texture(arr, default, n_items, from_bounds): """Prepare data to be uploaded as a texture. @@ -45,7 +43,7 @@ def _get_texture(arr, default, n_items, from_bounds): assert np.all(arr <= M) arr = (arr - m) / (M - m) assert np.all(arr >= 0) - assert np.all(arr <= 1.) + assert np.all(arr <= 1.0) return arr @@ -55,9 +53,7 @@ def _get_array(val, shape, default=None, dtype=np.float64): if hasattr(val, '__len__') and len(val) == 0: # pragma: no cover val = None # Do nothing if the array is already correct. - if (isinstance(val, np.ndarray) and - val.shape == shape and - val.dtype == dtype): + if isinstance(val, np.ndarray) and val.shape == shape and val.dtype == dtype: return val out = np.zeros(shape, dtype=dtype) # This solves `ValueError: could not broadcast input array from shape (n) @@ -100,10 +96,10 @@ def get_linear_x(n_signals, n_samples): Return a `(n_signals, n_samples)` array. """ - return np.tile(np.linspace(-1., 1., n_samples), (n_signals, 1)) + return np.tile(np.linspace(-1.0, 1.0, n_samples), (n_signals, 1)) -class BatchAccumulator(object): +class BatchAccumulator: """Accumulate data arrays for batch visuals. This class is used to simplify the creation of batch visuals, where different visual elements @@ -190,9 +186,10 @@ def data(self): return Bunch({key: getattr(self, key) for key in self.items.keys()}) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Misc -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _load_shader(filename): """Load a shader file.""" @@ -235,6 +232,7 @@ def _tesselate_histogram(hist): def _in_polygon(points, polygon): """Return the points that are inside a polygon.""" from matplotlib.path import Path + points = _as_array(points) polygon = _as_array(polygon) assert points.ndim == 2 diff --git a/phy/plot/visuals.py b/phy/plot/visuals.py index 178770d46..b56be92d7 100644 --- a/phy/plot/visuals.py +++ b/phy/plot/visuals.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """Common visuals. All visuals derive from the base class `BaseVisual()`. They all follow the same structure. @@ -10,36 +8,36 @@ """ -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import gzip from pathlib import Path import numpy as np - -from .base import BaseVisual -from .gloo import gl -from .transform import NDC -from .utils import ( - _tesselate_histogram, _get_texture, _get_array, _get_pos, _get_index) -from phy.gui.qt import is_high_dpi from phylib.io.array import _as_array from phylib.utils import Bunch from phylib.utils.geometry import _get_data_bounds +from phy.gui.qt import is_high_dpi + +from .base import BaseVisual +from .gloo import gl +from .transform import NDC +from .utils import _get_array, _get_index, _get_pos, _get_texture, _tesselate_histogram -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Utils -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ -DEFAULT_COLOR = (0.03, 0.57, 0.98, .75) +DEFAULT_COLOR = (0.03, 0.57, 0.98, 0.75) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Patch visual -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + class PatchVisual(BaseVisual): """Patch visual, displaying an arbitrary filled shape. @@ -64,10 +62,11 @@ class PatchVisual(BaseVisual): data_bounds : array-like (2D, shape[1] == 4) """ + default_color = DEFAULT_COLOR def __init__(self, primitive_type='triangle_fan'): - super(PatchVisual, self).__init__() + super().__init__() self.set_shader('patch') self.set_primitive_type(primitive_type) self.set_data_range(NDC) @@ -77,8 +76,8 @@ def vertex_count(self, x=None, y=None, pos=None, **kwargs): return y.size if y is not None else len(pos) def validate( - self, x=None, y=None, pos=None, color=None, depth=None, - data_bounds=None, **kwargs): + self, x=None, y=None, pos=None, color=None, depth=None, data_bounds=None, **kwargs + ): """Validate the requested data before passing it to set_data().""" if pos is None: x, y = _get_pos(x, y) @@ -96,8 +95,8 @@ def validate( assert data_bounds.shape[0] == n return Bunch( - pos=pos, color=color, depth=depth, data_bounds=data_bounds, - _n_items=n, _n_vertices=n) + pos=pos, color=color, depth=depth, data_bounds=data_bounds, _n_items=n, _n_vertices=n + ) def set_data(self, *args, **kwargs): """Update the visual data.""" @@ -120,9 +119,10 @@ def set_color(self, color): self.program['a_color'] = color.astype(np.float32) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Scatter visuals -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + class ScatterVisual(BaseVisual): """Scatter visual, displaying a fixed marker at various positions, colors, and marker sizes. @@ -147,8 +147,9 @@ class ScatterVisual(BaseVisual): data_bounds : array-like (2D, shape[1] == 4) """ + _init_keywords = ('marker',) - default_marker_size = 10. + default_marker_size = 10.0 default_marker = 'disc' default_color = DEFAULT_COLOR _supported_markers = ( @@ -174,7 +175,7 @@ class ScatterVisual(BaseVisual): ) def __init__(self, marker=None, marker_scaling=None): - super(ScatterVisual, self).__init__() + super().__init__() # Set the marker type. self.marker = marker or self.default_marker @@ -192,8 +193,16 @@ def vertex_count(self, x=None, y=None, pos=None, **kwargs): return y.size if y is not None else len(pos) def validate( - self, x=None, y=None, pos=None, color=None, size=None, depth=None, - data_bounds=None, **kwargs): + self, + x=None, + y=None, + pos=None, + color=None, + size=None, + depth=None, + data_bounds=None, + **kwargs, + ): """Validate the requested data before passing it to set_data().""" if pos is None: x, y = _get_pos(x, y) @@ -212,8 +221,14 @@ def validate( assert data_bounds.shape[0] == n return Bunch( - pos=pos, color=color, size=size, depth=depth, data_bounds=data_bounds, - _n_items=n, _n_vertices=n) + pos=pos, + color=color, + size=size, + depth=depth, + data_bounds=data_bounds, + _n_items=n, + _n_vertices=n, + ) def set_data(self, *args, **kwargs): """Update the visual data.""" @@ -266,7 +281,7 @@ class UniformScatterVisual(BaseVisual): """ _init_keywords = ('marker', 'color', 'size') - default_marker_size = 10. + default_marker_size = 10.0 default_marker = 'disc' default_color = DEFAULT_COLOR _supported_markers = ( @@ -292,7 +307,7 @@ class UniformScatterVisual(BaseVisual): ) def __init__(self, marker=None, color=None, size=None): - super(UniformScatterVisual, self).__init__() + super().__init__() # Set the marker type. self.marker = marker or self.default_marker @@ -321,11 +336,11 @@ def validate(self, x=None, y=None, pos=None, masks=None, data_bounds=None, **kwa assert pos.shape[1] == 2 n = pos.shape[0] - masks = _get_array(masks, (n, 1), 1., np.float32) + masks = _get_array(masks, (n, 1), 1.0, np.float32) assert masks.shape == (n, 1) # The mask is clu_idx + fractional mask - masks *= .99999 + masks *= 0.99999 # Validate the data. if data_bounds is not None: @@ -356,9 +371,10 @@ def set_data(self, *args, **kwargs): return data -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Plot visuals -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _as_list(arr): if isinstance(arr, np.ndarray): @@ -400,21 +416,22 @@ class PlotVisual(BaseVisual): _noconcat = ('x', 'y') def __init__(self): - super(PlotVisual, self).__init__() + super().__init__() self.set_shader('plot') self.set_primitive_type('line_strip') self.set_data_range(NDC) def validate( - self, x=None, y=None, color=None, depth=None, masks=None, data_bounds=None, **kwargs): + self, x=None, y=None, color=None, depth=None, masks=None, data_bounds=None, **kwargs + ): """Validate the requested data before passing it to set_data().""" assert y is not None y = _as_list(y) if x is None: - x = [np.linspace(-1., 1., len(_)) for _ in y] + x = [np.linspace(-1.0, 1.0, len(_)) for _ in y] x = _as_list(x) # Remove empty elements. @@ -431,15 +448,17 @@ def validate( ymax = [_max(_) for _ in y] data_bounds = np.c_[xmin, ymin, xmax, ymax] - color = _get_array(color, (n_signals, 4), - PlotVisual.default_color, - dtype=np.float32, - ) + color = _get_array( + color, + (n_signals, 4), + PlotVisual.default_color, + dtype=np.float32, + ) assert color.shape == (n_signals, 4) - masks = _get_array(masks, (n_signals, 1), 1., np.float32) + masks = _get_array(masks, (n_signals, 1), 1.0, np.float32) # The mask is clu_idx + fractional mask - masks *= .99999 + masks *= 0.99999 assert masks.shape == (n_signals, 1) depth = _get_array(depth, (n_signals, 1), 0) @@ -451,8 +470,15 @@ def validate( assert data_bounds.shape == (n_signals, 4) return Bunch( - x=x, y=y, color=color, depth=depth, data_bounds=data_bounds, masks=masks, - _n_items=n_signals, _n_vertices=self.vertex_count(y=y)) + x=x, + y=y, + color=color, + depth=depth, + data_bounds=data_bounds, + masks=masks, + _n_items=n_signals, + _n_vertices=self.vertex_count(y=y), + ) def set_color(self, color): """Update the visual's color.""" @@ -545,7 +571,7 @@ class UniformPlotVisual(BaseVisual): _noconcat = ('x', 'y') def __init__(self, color=None, depth=None): - super(UniformPlotVisual, self).__init__() + super().__init__() self.set_shader('uni_plot') self.set_primitive_type('line_strip') @@ -559,7 +585,7 @@ def validate(self, x=None, y=None, masks=None, data_bounds=None, **kwargs): y = _as_list(y) if x is None: - x = [np.linspace(-1., 1., len(_)) for _ in y] + x = [np.linspace(-1.0, 1.0, len(_)) for _ in y] x = _as_list(x) # Remove empty elements. @@ -569,9 +595,9 @@ def validate(self, x=None, y=None, masks=None, data_bounds=None, **kwargs): n_signals = len(x) - masks = _get_array(masks, (n_signals, 1), 1., np.float32) + masks = _get_array(masks, (n_signals, 1), 1.0, np.float32) # The mask is clu_idx + fractional mask - masks *= .99999 + masks *= 0.99999 assert masks.shape == (n_signals, 1) if isinstance(data_bounds, str) and data_bounds == 'auto': @@ -587,8 +613,13 @@ def validate(self, x=None, y=None, masks=None, data_bounds=None, **kwargs): assert data_bounds.shape == (n_signals, 4) return Bunch( - x=x, y=y, masks=masks, data_bounds=data_bounds, - _n_items=n_signals, _n_vertices=self.vertex_count(y=y)) + x=x, + y=y, + masks=masks, + data_bounds=data_bounds, + _n_items=n_signals, + _n_vertices=self.vertex_count(y=y), + ) def vertex_count(self, y=None, **kwargs): """Number of vertices for the requested data.""" @@ -643,9 +674,10 @@ def set_data(self, *args, **kwargs): return data -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Histogram visual -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + class HistogramVisual(BaseVisual): """A histogram visual. @@ -663,7 +695,7 @@ class HistogramVisual(BaseVisual): default_color = DEFAULT_COLOR def __init__(self): - super(HistogramVisual, self).__init__() + super().__init__() self.set_shader('histogram') self.set_primitive_type('triangles') @@ -683,7 +715,7 @@ def validate(self, hist=None, color=None, ylim=None, **kwargs): # Validate ylim. if ylim is None: - ylim = hist.max() if hist.size > 0 else 1. + ylim = hist.max() if hist.size > 0 else 1.0 ylim = np.atleast_1d(ylim) if len(ylim) == 1: ylim = np.tile(ylim, n_hists) @@ -692,8 +724,12 @@ def validate(self, hist=None, color=None, ylim=None, **kwargs): assert ylim.shape == (n_hists, 1) return Bunch( - hist=hist, ylim=ylim, color=color, - _n_items=n_hists, _n_vertices=self.vertex_count(hist)) + hist=hist, + ylim=ylim, + color=color, + _n_items=n_hists, + _n_vertices=self.vertex_count(hist), + ) def vertex_count(self, hist, **kwargs): """Number of vertices for the requested data.""" @@ -735,9 +771,9 @@ def set_data(self, *args, **kwargs): return data -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test visual -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ FONT_MAP_PATH = Path(__file__).parent / 'static/SourceCodePro-Regular.npy.gz' FONT_MAP_SIZE = (6, 16) @@ -779,13 +815,14 @@ class TextVisual(BaseVisual): data_bounds : array-like (2D, shape[1] == 4) """ - default_color = (1., 1., 1., 1.) - default_font_size = 6. + + default_color = (1.0, 1.0, 1.0, 1.0) + default_font_size = 6.0 _init_keywords = ('color',) _noconcat = ('text',) def __init__(self, color=None, font_size=None): - super(TextVisual, self).__init__() + super().__init__() self.set_shader('msdf') self.set_primitive_type('triangles') self.set_data_range(NDC) @@ -809,8 +846,7 @@ def __init__(self, color=None, font_size=None): def _get_glyph_indices(self, s): return [FONT_MAP_CHARS.index(char) for char in s] - def validate( - self, pos=None, text=None, color=None, anchor=None, data_bounds=None, **kwargs): + def validate(self, pos=None, text=None, color=None, anchor=None, data_bounds=None, **kwargs): """Validate the requested data before passing it to set_data().""" if text is None: @@ -835,7 +871,7 @@ def validate( assert color.shape[1] == 4 assert len(color) == n_text - anchor = anchor if anchor is not None else (0., 0.) + anchor = anchor if anchor is not None else (0.0, 0.0) anchor = np.atleast_2d(anchor) if anchor.shape[0] == 1: anchor = np.repeat(anchor, n_text, axis=0) @@ -849,8 +885,14 @@ def validate( assert data_bounds.shape == (n_text, 4) return Bunch( - pos=pos, text=text, anchor=anchor, data_bounds=data_bounds, color=color, - _n_items=n_text, _n_vertices=self.vertex_count(text=text)) + pos=pos, + text=text, + anchor=anchor, + data_bounds=data_bounds, + color=color, + _n_items=n_text, + _n_vertices=self.vertex_count(text=text), + ) def vertex_count(self, **kwargs): """Number of vertices for the requested data.""" @@ -948,12 +990,13 @@ def set_data(self, *args, **kwargs): def on_draw(self): # NOTE: use linear interpolation for the SDF texture. self.program._uniforms['u_tex']._data.set_interpolation('linear') - super(TextVisual, self).on_draw() + super().on_draw() -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Line visual -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + class LineVisual(BaseVisual): """Line segments. @@ -966,11 +1009,11 @@ class LineVisual(BaseVisual): """ - default_color = (.3, .3, .3, 1.) + default_color = (0.3, 0.3, 0.3, 1.0) _init_keywords = ('color',) def __init__(self): - super(LineVisual, self).__init__() + super().__init__() self.set_shader('line') self.set_primitive_type('lines') self.set_data_range(NDC) @@ -995,8 +1038,12 @@ def validate(self, pos=None, color=None, data_bounds=None, **kwargs): assert data_bounds.shape == (n_lines, 4) return Bunch( - pos=pos, color=color, data_bounds=data_bounds, - _n_items=n_lines, _n_vertices=self.vertex_count(pos=pos)) + pos=pos, + color=color, + data_bounds=data_bounds, + _n_items=n_lines, + _n_vertices=self.vertex_count(pos=pos), + ) def vertex_count(self, pos=None, **kwargs): """Number of vertices for the requested data.""" @@ -1035,9 +1082,10 @@ def set_data(self, *args, **kwargs): return data -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Agg line visual -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def ortho(left, right, bottom, top, znear, zfar): # pragma: no cover """Create orthographic projection matrix @@ -1062,9 +1110,9 @@ def ortho(left, right, bottom, top, znear, zfar): # pragma: no cover M : array Orthographic projection matrix (4x4). """ - assert(right != left) - assert(bottom != top) - assert(znear != zfar) + assert right != left + assert bottom != top + assert znear != zfar M = np.zeros((4, 4), dtype=np.float32) M[0, 0] = +2.0 / (right - left) @@ -1091,11 +1139,11 @@ class LineAggGeomVisual(BaseVisual): # pragma: no cover """ - default_color = (.75, .75, .75, 1.) + default_color = (0.75, 0.75, 0.75, 1.0) _init_keywords = ('color',) def __init__(self): - super(LineAggGeomVisual, self).__init__() + super().__init__() self.set_shader('line_agg_geom') self.set_primitive_type('line_strip_adjacency_ext') # Geometry shader params. @@ -1107,17 +1155,17 @@ def __init__(self): def _get_index_buffer(self, P, closed=True): if closed: if np.allclose(P[0], P[1]): - I = (np.arange(len(P) + 2) - 1) + I = np.arange(len(P) + 2) - 1 I[0], I[-1] = 0, len(P) - 1 else: - I = (np.arange(len(P) + 3) - 1) + I = np.arange(len(P) + 3) - 1 I[0], I[-2], I[-1] = len(P) - 1, 0, 1 else: - I = (np.arange(len(P) + 2) - 1) + I = np.arange(len(P) + 2) - 1 I[0], I[-1] = 0, len(P) - 1 return I - def validate(self, pos=None, color=None, line_width=10., data_bounds=None, **kwargs): + def validate(self, pos=None, color=None, line_width=10.0, data_bounds=None, **kwargs): """Validate the requested data before passing it to set_data().""" assert pos is not None pos = _as_array(pos) @@ -1138,8 +1186,13 @@ def validate(self, pos=None, color=None, line_width=10., data_bounds=None, **kwa # assert data_bounds.shape == (n_lines, 2) return Bunch( - pos=pos, color=color, line_width=line_width, data_bounds=data_bounds, - _n_items=1, _n_vertices=self.vertex_count(pos=pos)) + pos=pos, + color=color, + line_width=line_width, + data_bounds=data_bounds, + _n_items=1, + _n_vertices=self.vertex_count(pos=pos), + ) def vertex_count(self, pos=None, **kwargs): """Number of vertices for the requested data.""" @@ -1199,7 +1252,7 @@ class PlotAggVisual(BaseVisual): _noconcat = ('x', 'y') def __init__(self, line_width=None, closed=False): - super(PlotAggVisual, self).__init__() + super().__init__() self.set_shader('plot_agg') self.set_primitive_type('triangle_strip') @@ -1208,14 +1261,15 @@ def __init__(self, line_width=None, closed=False): self.line_width = line_width or self.default_line_width def validate( - self, x=None, y=None, color=None, depth=None, masks=None, data_bounds=None, **kwargs): + self, x=None, y=None, color=None, depth=None, masks=None, data_bounds=None, **kwargs + ): """Validate the requested data before passing it to set_data().""" assert y is not None y = np.atleast_2d(_as_array(y)) n_signals, n_samples = y.shape if x is None: - x = np.tile(np.linspace(-1., 1., n_samples), (n_signals, 1)) + x = np.tile(np.linspace(-1.0, 1.0, n_samples), (n_signals, 1)) x = np.atleast_2d(_as_array(x)) if isinstance(data_bounds, str) and data_bounds == 'auto': @@ -1223,15 +1277,17 @@ def validate( ymin, ymax = y.min(axis=1), y.max(axis=1) data_bounds = np.c_[xmin, ymin, xmax, ymax] - color = _get_array(color, (n_signals, 4), - PlotVisual.default_color, - dtype=np.float32, - ) + color = _get_array( + color, + (n_signals, 4), + PlotVisual.default_color, + dtype=np.float32, + ) assert color.shape == (n_signals, 4) - masks = _get_array(masks, (n_signals, 1), 1., np.float32) + masks = _get_array(masks, (n_signals, 1), 1.0, np.float32) # The mask is clu_idx + fractional mask - masks *= .99999 + masks *= 0.99999 assert masks.shape == (n_signals, 1) depth = _get_array(depth, (n_signals, 1), 0) @@ -1243,8 +1299,15 @@ def validate( assert data_bounds.shape == (n_signals, 4) return Bunch( - x=x, y=y, color=color, depth=depth, masks=masks, data_bounds=data_bounds, - _n_items=n_signals, _n_vertices=self.vertex_count(y=y)) + x=x, + y=y, + color=color, + depth=depth, + masks=masks, + data_bounds=data_bounds, + _n_items=n_signals, + _n_vertices=self.vertex_count(y=y), + ) def vertex_count(self, y=None, **kwargs): """Number of vertices for the requested data.""" @@ -1376,9 +1439,10 @@ def set_data(self, *args, **kwargs): return data -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Image visual -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + class ImageVisual(BaseVisual): """Display a 2D image. @@ -1390,7 +1454,7 @@ class ImageVisual(BaseVisual): """ def __init__(self): - super(ImageVisual, self).__init__() + super().__init__() self.set_shader('image') self.set_primitive_type('triangles') @@ -1413,22 +1477,26 @@ def set_data(self, *args, **kwargs): self.n_vertices = self.vertex_count(**data) image = data.image - pos = np.array([ - [-1, -1], - [-1, +1], - [+1, -1], - [-1, +1], - [+1, +1], - [+1, -1], - ]) - tex_coords = np.array([ - [0, 1], - [0, 0], - [+1, 1], - [0, 0], - [+1, 0], - [+1, 1], - ]) + pos = np.array( + [ + [-1, -1], + [-1, +1], + [+1, -1], + [-1, +1], + [+1, +1], + [+1, -1], + ] + ) + tex_coords = np.array( + [ + [0, 1], + [0, 0], + [+1, 1], + [0, 0], + [+1, 0], + [+1, 1], + ] + ) self.program['a_position'] = pos.astype(np.float32) self.program['a_tex_coords'] = tex_coords.astype(np.float32) self.program['u_tex'] = image.astype(np.float32) @@ -1437,9 +1505,10 @@ def set_data(self, *args, **kwargs): return data -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Polygon visual -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + class PolygonVisual(BaseVisual): """Polygon. @@ -1450,10 +1519,11 @@ class PolygonVisual(BaseVisual): data_bounds : array-like (2D, shape[1] == 4) """ + default_color = (1, 1, 1, 1) def __init__(self): - super(PolygonVisual, self).__init__() + super().__init__() self.set_shader('polygon') self.set_primitive_type('line_loop') self.set_data_range(NDC) @@ -1473,8 +1543,11 @@ def validate(self, pos=None, data_bounds=None, **kwargs): assert data_bounds.shape == (1, 4) return Bunch( - pos=pos, data_bounds=data_bounds, - _n_items=pos.shape[0], _n_vertices=self.vertex_count(pos=pos)) + pos=pos, + data_bounds=data_bounds, + _n_items=pos.shape[0], + _n_vertices=self.vertex_count(pos=pos), + ) def vertex_count(self, pos=None, **kwargs): """Number of vertices for the requested data.""" diff --git a/phy/utils/__init__.py b/phy/utils/__init__.py index 624ba79a7..e449e02ff 100644 --- a/phy/utils/__init__.py +++ b/phy/utils/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # flake8: noqa """Utilities: plugin system, event system, configuration system, profiling, debugging, cacheing, @@ -8,12 +7,23 @@ from .plugin import IPlugin, attach_plugins from .config import ensure_dir_exists, load_master_config, phy_config_dir from .context import Context -from .color import( - colormaps, selected_cluster_color, add_alpha, ClusterColorSelector -) +from .color import colormaps, selected_cluster_color, add_alpha, ClusterColorSelector from phylib.utils import ( - Bunch, emit, connect, unconnect, silent, reset, set_silent, - load_json, save_json, load_pickle, save_pickle, read_python, - read_text, write_text, read_tsv, write_tsv, + Bunch, + emit, + connect, + unconnect, + silent, + reset, + set_silent, + load_json, + save_json, + load_pickle, + save_pickle, + read_python, + read_text, + write_text, + read_tsv, + write_tsv, ) diff --git a/phy/utils/color.py b/phy/utils/color.py index a99e81324..73fe16ce4 100644 --- a/phy/utils/color.py +++ b/phy/utils/color.py @@ -1,29 +1,27 @@ -# -*- coding: utf-8 -*- - """Color routines.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ -import colorcet as cc import logging -from phylib.utils import Bunch -from phylib.io.array import _index_of - +import colorcet as cc import numpy as np -from numpy.random import uniform from matplotlib.colors import hsv_to_rgb, rgb_to_hsv +from numpy.random import uniform +from phylib.io.array import _index_of +from phylib.utils import Bunch logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Random colors -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ -def _random_color(h_range=(0., 1.), s_range=(.5, 1.), v_range=(.5, 1.)): + +def _random_color(h_range=(0.0, 1.0), s_range=(0.5, 1.0), v_range=(0.5, 1.0)): """Generate a random RGB color.""" h, s, v = uniform(*h_range), uniform(*s_range), uniform(*v_range) r, g, b = hsv_to_rgb(np.array([[[h, s, v]]])).flat @@ -36,10 +34,7 @@ def _is_bright(rgb): """ L = 0 for c, coeff in zip(rgb, (0.2126, 0.7152, 0.0722)): - if c <= 0.03928: - c = c / 12.92 - else: - c = ((c + 0.055) / 1.055) ** 2.4 + c = c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4 L += c * coeff if (L + 0.05) / (0.0 + 0.05) > (1.0 + 0.05) / (L + 0.05): return True @@ -57,7 +52,7 @@ def _hex_to_triplet(h): """Convert an hexadecimal color to a triplet of int8 integers.""" if h.startswith('#'): h = h[1:] - return tuple(int(h[i:i + 2], 16) for i in (0, 2, 4)) + return tuple(int(h[i : i + 2], 16) for i in (0, 2, 4)) def _override_hsv(rgb, h=None, s=None, v=None): @@ -69,9 +64,10 @@ def _override_hsv(rgb, h=None, s=None, v=None): return r, g, b -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Colormap utilities -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _selected_cluster_idx(selected_clusters, cluster_ids): selected_clusters = np.asarray(selected_clusters, dtype=np.int32) @@ -115,9 +111,10 @@ def _categorical_colormap(colormap, values, vmin=None, vmax=None, categorize=Non return colormap[x % n, :] -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Colormaps -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + # Default color map for the selected clusters. # see https://colorcet.pyviz.org/user_guide/Categorical.html @@ -134,17 +131,19 @@ def _make_default_colormap(): def _make_cluster_group_colormap(): """Return cluster group colormap.""" - return np.array([ - [0.4, 0.4, 0.4], # noise - [0.5, 0.5, 0.5], # mua - [0.5254, 0.8196, 0.42745], # good - [0.75, 0.75, 0.75], # '' (None = '' = unsorted) - ]) + return np.array( + [ + [0.4, 0.4, 0.4], # noise + [0.5, 0.5, 0.5], # mua + [0.5254, 0.8196, 0.42745], # good + [0.75, 0.75, 0.75], # '' (None = '' = unsorted) + ] + ) """Built-in colormaps.""" colormaps = Bunch( - blank=np.array([[.75, .75, .75]]), + blank=np.array([[0.75, 0.75, 0.75]]), default=_make_default_colormap(), cluster_group=_make_cluster_group_colormap(), categorical=np.array(cc.glasbey_bw_minc_20_minl_30), @@ -154,7 +153,7 @@ def _make_cluster_group_colormap(): ) -def selected_cluster_color(i, alpha=1.): +def selected_cluster_color(i, alpha=1.0): """Return the color, as a 4-tuple, of the i-th selected cluster.""" return add_alpha(tuple(colormaps.default[i % len(colormaps.default)]), alpha=alpha) @@ -194,11 +193,12 @@ def _add_selected_clusters_colors(selected_clusters, cluster_ids, cluster_colors return cluster_colors -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Cluster color selector -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ -def add_alpha(c, alpha=1.): + +def add_alpha(c, alpha=1.0): """Add an alpha channel to an RGB color. Parameters @@ -216,11 +216,11 @@ def add_alpha(c, alpha=1.): if c.shape[-1] == 4: c = c[..., :3] assert c.shape[-1] == 3 - out = np.concatenate([c, alpha * np.ones((c.shape[:-1] + (1,)))], axis=-1) + out = np.concatenate([c, alpha * np.ones(c.shape[:-1] + (1,))], axis=-1) assert out.ndim == c.ndim assert out.shape[-1] == c.shape[-1] + 1 return out - raise ValueError("Unknown value given in add_alpha().") # pragma: no cover + raise ValueError('Unknown value given in add_alpha().') # pragma: no cover def _categorize(values): @@ -233,21 +233,23 @@ def _categorize(values): return values -class ClusterColorSelector(object): +class ClusterColorSelector: """Assign a color to clusters depending on cluster labels or metrics.""" + _colormap = colormaps.categorical _categorical = True _logarithmic = False def __init__( - self, fun=None, colormap=None, categorical=None, logarithmic=None, cluster_ids=None): + self, fun=None, colormap=None, categorical=None, logarithmic=None, cluster_ids=None + ): self.cluster_ids = cluster_ids if cluster_ids is not None else () self._fun = fun self.set_color_mapping( - fun=fun, colormap=colormap, categorical=categorical, logarithmic=logarithmic) + fun=fun, colormap=colormap, categorical=categorical, logarithmic=logarithmic + ) - def set_color_mapping( - self, fun=None, colormap=None, categorical=None, logarithmic=None): + def set_color_mapping(self, fun=None, colormap=None, categorical=None, logarithmic=None): """Set the field used to choose the cluster colors, and the associated colormap. Parameters @@ -304,14 +306,16 @@ def map(self, values): vmin, vmax = self.vmin, self.vmax assert values is not None # Use categorical or continuous colormap depending on the categorical option. - f = (_categorical_colormap - if self._categorical and np.issubdtype(values.dtype, np.integer) - else _continuous_colormap) + f = ( + _categorical_colormap + if self._categorical and np.issubdtype(values.dtype, np.integer) + else _continuous_colormap + ) return f(self._colormap, values, vmin=vmin, vmax=vmax) def _get_cluster_value(self, cluster_id): """Return the field value for a given cluster.""" - return self._fun(cluster_id) if hasattr(self._fun, '__call__') else self._fun or 0 + return self._fun(cluster_id) if callable(self._fun) else self._fun or 0 def get(self, cluster_id, alpha=None): """Return the RGBA color of a single cluster.""" @@ -330,7 +334,7 @@ def get_values(self, cluster_ids): values = _categorize(values) return np.array(values) - def get_colors(self, cluster_ids, alpha=1.): + def get_colors(self, cluster_ids, alpha=1.0): """Return the RGBA colors of some clusters.""" values = self.get_values(cluster_ids) assert values is not None diff --git a/phy/utils/config.py b/phy/utils/config.py index db866c65e..425606d7c 100644 --- a/phy/utils/config.py +++ b/phy/utils/config.py @@ -1,24 +1,23 @@ -# -*- coding: utf-8 -*- - """Configuration utilities based on the traitlets package.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import logging from pathlib import Path from textwrap import dedent -from traitlets.config import Config, PyFileConfigLoader, JSONFileConfigLoader from phylib.utils._misc import ensure_dir_exists, phy_config_dir +from traitlets.config import Config, JSONFileConfigLoader, PyFileConfigLoader logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Config -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def load_config(path=None): """Load a Python or JSON config file and return a `Config` instance.""" @@ -28,7 +27,7 @@ def load_config(path=None): if not path.exists(): # pragma: no cover return Config() file_ext = path.suffix - logger.debug("Load config file `%s`.", path) + logger.debug('Load config file `%s`.', path) if file_ext == '.py': config = PyFileConfigLoader(path.name, str(path.parent), log=logger).load_config() elif file_ext == '.json': @@ -42,7 +41,7 @@ def _default_config(config_dir=None): if not config_dir: # pragma: no cover config_dir = Path.home() / '.phy' path = config_dir / 'plugins' - return dedent(""" + return dedent(f""" # You can also put your plugins in ~/.phy/plugins/. from phy import IPlugin @@ -55,8 +54,8 @@ def _default_config(config_dir=None): # pass c = get_config() - c.Plugins.dirs = [r'{}'] - """.format(path)) + c.Plugins.dirs = [r'{path}'] + """) def load_master_config(config_dir=None): @@ -67,7 +66,7 @@ def load_master_config(config_dir=None): # Create a default config file if necessary. if not path.exists(): ensure_dir_exists(path.parent) - logger.debug("Creating default phy config file at `%s`.", path) + logger.debug('Creating default phy config file at `%s`.', path) path.write_text(_default_config(config_dir=config_dir)) assert path.exists() try: @@ -80,6 +79,7 @@ def load_master_config(config_dir=None): def save_config(path, config): """Save a Config instance to a JSON file.""" import json + config['version'] = 1 with open(path, 'w') as f: json.dump(config, f) diff --git a/phy/utils/context.py b/phy/utils/context.py index 1f450d3a3..df7d747a4 100644 --- a/phy/utils/context.py +++ b/phy/utils/context.py @@ -1,27 +1,27 @@ -# -*- coding: utf-8 -*- - """Execution context that handles parallel processing and caching.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ -from functools import wraps import inspect import logging import os +from functools import wraps from pathlib import Path from pickle import dump, load -from phylib.utils._misc import save_json, load_json, load_pickle, save_pickle, _fullname -from .config import phy_config_dir, ensure_dir_exists +from phylib.utils._misc import _fullname, load_json, load_pickle, save_json, save_pickle + +from .config import ensure_dir_exists, phy_config_dir logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Context -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _cache_methods(obj, memcached, cached): # pragma: no cover for name in memcached: @@ -33,7 +33,7 @@ def _cache_methods(obj, memcached, cached): # pragma: no cover setattr(obj, name, obj.context.cache(f)) -class Context(object): +class Context: """Handle function disk and memory caching with joblib. Memcaching a function is used to save *in memory* the output of the function for all @@ -70,14 +70,14 @@ def my_function(x): """ """Maximum cache size, in bytes.""" - cache_limit = 2 * 1024 ** 3 # 2 GB + cache_limit = 2 * 1024**3 # 2 GB def __init__(self, cache_dir, verbose=0): self.verbose = verbose # Make sure the cache directory exists. self.cache_dir = Path(cache_dir).expanduser() if not self.cache_dir.exists(): - logger.debug("Create cache directory `%s`.", self.cache_dir) + logger.debug('Create cache directory `%s`.', self.cache_dir) os.makedirs(str(self.cache_dir)) # Ensure the memcache directory exists. @@ -94,36 +94,31 @@ def _set_memory(self, cache_dir): # Try importing joblib. try: from joblib import Memory - self._memory = Memory( - location=self.cache_dir, mmap_mode=None, verbose=self.verbose, - bytes_limit=self.cache_limit) - logger.debug("Initialize joblib cache dir at `%s`.", self.cache_dir) - logger.debug("Reducing the size of the cache if needed.") + + self._memory = Memory(location=self.cache_dir, mmap_mode=None, verbose=self.verbose) + logger.debug('Initialize joblib cache dir at `%s`.', self.cache_dir) + logger.debug('Reducing the size of the cache if needed.') self._memory.reduce_size() except ImportError: # pragma: no cover - logger.warning( - "Joblib is not installed. Install it with `conda install joblib`.") + logger.warning('Joblib is not installed. Install it with `conda install joblib`.') self._memory = None def cache(self, f): """Cache a function using the context's cache directory.""" if self._memory is None: # pragma: no cover - logger.debug("Joblib is not installed: skipping caching.") + logger.debug('Joblib is not installed: skipping caching.') return f assert f # NOTE: discard self in instance methods. - if 'self' in inspect.getfullargspec(f).args: - ignore = ['self'] - else: - ignore = None + ignore = ['self'] if 'self' in inspect.getfullargspec(f).args else None disk_cached = self._memory.cache(f, ignore=ignore) return disk_cached def load_memcache(self, name): """Load the memcache from disk (pickle file), if it exists.""" - path = self.cache_dir / 'memcache' / (name + '.pkl') + path = self.cache_dir / 'memcache' / (f'{name}.pkl') if path.exists(): - logger.debug("Load memcache for `%s`.", name) + logger.debug('Load memcache for `%s`.', name) with open(str(path), 'rb') as fd: cache = load(fd) else: @@ -134,8 +129,8 @@ def load_memcache(self, name): def save_memcache(self): """Save the memcache to disk using pickle.""" for name, cache in self._memcache.items(): - path = self.cache_dir / 'memcache' / (name + '.pkl') - logger.debug("Save memcache for `%s`.", name) + path = self.cache_dir / 'memcache' / (f'{name}.pkl') + logger.debug('Save memcache for `%s`.', name) with open(str(path), 'wb') as fd: dump(cache, fd) @@ -154,6 +149,7 @@ def memcached(*args, **kwargs): out = f(*args, **kwargs) cache[h] = out return out + return memcached def _get_path(self, name, location, file_ext='.json'): @@ -182,7 +178,7 @@ def save(self, name, data, location='local', kind='json'): file_ext = '.json' if kind == 'json' else '.pkl' path = self._get_path(name, location, file_ext=file_ext) ensure_dir_exists(path.parent) - logger.debug("Save data to `%s`.", path) + logger.debug('Save data to `%s`.', path) if kind == 'json': save_json(path, data) else: diff --git a/phy/utils/plugin.py b/phy/utils/plugin.py index 317e84caa..db5fb4e42 100644 --- a/phy/utils/plugin.py +++ b/phy/utils/plugin.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """Simple plugin system. Code from http://eli.thegreenplace.net/2012/08/07/fundamental-concepts-of-plugin-infrastructures @@ -7,9 +5,9 @@ """ -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import importlib import logging @@ -18,14 +16,16 @@ from pathlib import Path from phylib.utils._misc import _fullname + from .config import load_master_config logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # IPlugin interface -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + class IPluginRegistry(type): """Regjster all plugin instances.""" @@ -34,7 +34,7 @@ class IPluginRegistry(type): def __init__(cls, name, bases, attrs): if name != 'IPlugin': - logger.debug("Register plugin `%s`.", _fullname(cls)) + logger.debug('Register plugin `%s`.', _fullname(cls)) if _fullname(cls) not in (_fullname(_) for _ in IPluginRegistry.plugins): IPluginRegistry.plugins.append(cls) @@ -45,7 +45,6 @@ class IPlugin(metaclass=IPluginRegistry): Plugin classes should just implement a method `attach_to_controller(self, controller)`. """ - pass def get_plugin(name): @@ -53,12 +52,13 @@ def get_plugin(name): for plugin in IPluginRegistry.plugins: if name in plugin.__name__: return plugin - raise ValueError("The plugin %s cannot be found." % name) + raise ValueError(f'The plugin {name} cannot be found.') -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Plugins discovery -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _iter_plugin_files(dirs): """Iterate through all found plugin files.""" @@ -72,11 +72,11 @@ def _iter_plugin_files(dirs): base = subdir.name if 'test' in base or '__' in base or '.git' in str(subdir): # pragma: no cover continue - logger.debug("Scanning `%s`.", subdir) + logger.debug('Scanning `%s`.', subdir) for filename in files: - if (filename.startswith('__') or not filename.endswith('.py')): + if filename.startswith('__') or not filename.endswith('.py'): continue # pragma: no cover - logger.debug("Found plugin module `%s`.", filename) + logger.debug('Found plugin module `%s`.', filename) yield subdir / filename @@ -143,7 +143,7 @@ class name of the Controller instance, plus those specified in the plugins keywo default_plugins = c.plugins if c else [] if len(default_plugins): plugins = default_plugins + plugins - logger.debug("Loading %d plugins.", len(plugins)) + logger.debug('Loading %d plugins.', len(plugins)) attached = [] for plugin in plugins: try: @@ -154,8 +154,7 @@ class name of the Controller instance, plus those specified in the plugins keywo try: p.attach_to_controller(controller) attached.append(plugin) - logger.debug("Attached plugin %s.", plugin) + logger.debug('Attached plugin %s.', plugin) except Exception as e: # pragma: no cover - logger.warning( - "An error occurred when attaching plugin %s: %s.", plugin, e) + logger.warning('An error occurred when attaching plugin %s: %s.', plugin, e) return attached diff --git a/phy/utils/profiling.py b/phy/utils/profiling.py index de4468541..ddce4b73f 100644 --- a/phy/utils/profiling.py +++ b/phy/utils/profiling.py @@ -1,20 +1,18 @@ -# -*- coding: utf-8 -*- - """Utility functions used for tests.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import builtins -from contextlib import contextmanager -from cProfile import Profile import functools -from io import StringIO import logging import os -from pathlib import Path import sys +from contextlib import contextmanager +from cProfile import Profile +from io import StringIO +from pathlib import Path from timeit import default_timer from .config import ensure_dir_exists @@ -22,34 +20,35 @@ logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Profiling -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + @contextmanager def benchmark(name='', repeats=1): """Contexts manager to benchmark an action.""" start = default_timer() yield - duration = (default_timer() - start) * 1000. - logger.info("%s took %.6fms.", name, duration / repeats) + duration = (default_timer() - start) * 1000.0 + logger.info('%s took %.6fms.', name, duration / repeats) class ContextualProfile(Profile): # pragma: no cover """Class used for profiling.""" def __init__(self, *args, **kwds): - super(ContextualProfile, self).__init__(*args, **kwds) + super().__init__(*args, **kwds) self.enable_count = 0 def enable_by_count(self, subcalls=True, builtins=True): - """ Enable the profiler if it hasn't been enabled before.""" + """Enable the profiler if it hasn't been enabled before.""" if self.enable_count == 0: self.enable(subcalls=subcalls, builtins=builtins) self.enable_count += 1 def disable_by_count(self): - """ Disable the profiler if the number of disable requests matches the + """Disable the profiler if the number of disable requests matches the number of enable requests. """ if self.enable_count > 0: @@ -66,6 +65,7 @@ def __call__(self, func): def wrap_function(self, func): """Wrap a function to profile it.""" + @functools.wraps(func) def wrapper(*args, **kwds): self.enable_by_count() @@ -74,6 +74,7 @@ def wrapper(*args, **kwds): finally: self.disable_by_count() return result + return wrapper def __enter__(self): @@ -89,6 +90,7 @@ def _enable_profiler(line_by_line=False): # pragma: no cover return builtins.__dict__['profile'] if line_by_line: import line_profiler + prof = line_profiler.LineProfiler() else: prof = ContextualProfile() @@ -106,6 +108,7 @@ def _profile(prof, statement, glob, loc): sys.stdout = output = StringIO() try: # pragma: no cover from line_profiler import LineProfiler + if isinstance(prof, LineProfiler): prof.print_stats() else: @@ -126,8 +129,10 @@ def _profile(prof, statement, glob, loc): def _enable_pdb(): # pragma: no cover """Enable a Qt-aware IPython debugger.""" from IPython.core import ultratb - logger.debug("Enabling debugger.") + + logger.debug('Enabling debugger.') from PyQt5.QtCore import pyqtRemoveInputHook + pyqtRemoveInputHook() sys.excepthook = ultratb.FormattedTB(mode='Verbose', color_scheme='Linux', call_pdb=True) @@ -135,5 +140,6 @@ def _enable_pdb(): # pragma: no cover def _memory_usage(): # pragma: no cover """Get the memory usage of the current Python process.""" import psutil + process = psutil.Process(os.getpid()) return process.memory_info().rss diff --git a/phy/utils/tests/conftest.py b/phy/utils/tests/conftest.py index 27d6ee4c1..9a9ed31ac 100644 --- a/phy/utils/tests/conftest.py +++ b/phy/utils/tests/conftest.py @@ -1,17 +1,15 @@ -# -*- coding: utf-8 -*- - """py.test fixtures.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ from pytest import fixture - -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Common fixtures -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + @fixture def temp_config_dir(tempdir): diff --git a/phy/utils/tests/test_color.py b/phy/utils/tests/test_color.py index 076aa2b32..2e16179c5 100644 --- a/phy/utils/tests/test_color.py +++ b/phy/utils/tests/test_color.py @@ -1,26 +1,33 @@ -# -*- coding: utf-8 -*- - """Test colors.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import colorcet as cc import numpy as np from numpy.testing import assert_almost_equal as ae - from pytest import raises from ..color import ( - _is_bright, _random_bright_color, spike_colors, add_alpha, selected_cluster_color, - _override_hsv, _hex_to_triplet, _continuous_colormap, _categorical_colormap, - _selected_cluster_idx, ClusterColorSelector, _add_selected_clusters_colors) - - -#------------------------------------------------------------------------------ + ClusterColorSelector, + _add_selected_clusters_colors, + _categorical_colormap, + _continuous_colormap, + _hex_to_triplet, + _is_bright, + _override_hsv, + _random_bright_color, + _selected_cluster_idx, + add_alpha, + selected_cluster_color, + spike_colors, +) + +# ------------------------------------------------------------------------------ # Tests -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_random_color(): for _ in range(20): @@ -32,15 +39,15 @@ def test_hex_to_triplet(): def test_add_alpha(): - assert add_alpha((0, .5, 1), .75) == (0, .5, 1, .75) - assert add_alpha(np.random.rand(5, 3), .5).shape == (5, 4) + assert add_alpha((0, 0.5, 1), 0.75) == (0, 0.5, 1, 0.75) + assert add_alpha(np.random.rand(5, 3), 0.5).shape == (5, 4) - assert add_alpha((0, .5, 1, .1), .75) == (0, .5, 1, .75) - assert add_alpha(np.random.rand(5, 4), .5).shape == (5, 4) + assert add_alpha((0, 0.5, 1, 0.1), 0.75) == (0, 0.5, 1, 0.75) + assert add_alpha(np.random.rand(5, 4), 0.5).shape == (5, 4) def test_override_hsv(): - assert _override_hsv((.1, .9, .5), h=1, s=0, v=1) == (1, 1, 1) + assert _override_hsv((0.1, 0.9, 0.5), h=1, s=0, v=1) == (1, 1, 1) def test_selected_cluster_color(): @@ -71,9 +78,9 @@ def test_spike_colors(): def test_cluster_color_selector_1(): cluster_ids = [1, 2, 3] - c = ClusterColorSelector(lambda cid: cid * .1, cluster_ids=cluster_ids) + c = ClusterColorSelector(lambda cid: cid * 0.1, cluster_ids=cluster_ids) - assert len(c.get(1, alpha=.5)) == 4 + assert len(c.get(1, alpha=0.5)) == 4 ae(c.get_values([0, 0]), np.zeros(2)) for colormap in ('linear', 'rainbow', 'categorical', 'diverging'): @@ -85,7 +92,8 @@ def test_cluster_color_selector_1(): def test_cluster_color_selector_2(): cluster_ids = [2, 3, 5, 7] c = ClusterColorSelector( - lambda cid: cid, cluster_ids=cluster_ids, colormap='categorical', categorical=True) + lambda cid: cid, cluster_ids=cluster_ids, colormap='categorical', categorical=True + ) c2 = c.get_colors([2]) c3 = c.get_colors([3]) @@ -107,7 +115,8 @@ def test_cluster_color_group(): # Mock ClusterMeta instance, with 'fields' property and get(field, cluster) function. cluster_ids = [1, 2, 3] c = ClusterColorSelector( - lambda cl: {1: None, 2: 'mua', 3: 'good'}[cl], cluster_ids=cluster_ids) + lambda cl: {1: None, 2: 'mua', 3: 'good'}[cl], cluster_ids=cluster_ids + ) c.set_color_mapping(colormap='cluster_group') colors = c.get_colors(cluster_ids) @@ -116,7 +125,7 @@ def test_cluster_color_group(): def test_cluster_color_log(): cluster_ids = [1, 2, 3] - c = ClusterColorSelector(lambda cid: cid * .1, cluster_ids=cluster_ids) + c = ClusterColorSelector(lambda cid: cid * 0.1, cluster_ids=cluster_ids) c.set_color_mapping(logarithmic=True) colors = c.get_colors(cluster_ids) @@ -142,7 +151,8 @@ def test_add_selected_clusters_colors_2(): cluster_colors = np.c_[np.arange(5), np.zeros((5, 3))] cluster_colors_sel = _add_selected_clusters_colors( - selected_clusters, cluster_ids, cluster_colors) + selected_clusters, cluster_ids, cluster_colors + ) ae(cluster_colors_sel[[0, 1, 4]], cluster_colors[[0, 1, 4]]) ae(cluster_colors_sel[2], selected_cluster_color(0)) diff --git a/phy/utils/tests/test_config.py b/phy/utils/tests/test_config.py index f1660c71b..5d834176f 100644 --- a/phy/utils/tests/test_config.py +++ b/phy/utils/tests/test_config.py @@ -1,43 +1,44 @@ -# -*- coding: utf-8 -*- - """Test config.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import logging from textwrap import dedent +from phylib.utils._misc import write_text from pytest import fixture from traitlets import Float from traitlets.config import Configurable from .. import config as _config -from phylib.utils._misc import write_text -from ..config import (ensure_dir_exists, - load_config, - load_master_config, - save_config, - ) +from ..config import ( + ensure_dir_exists, + load_config, + load_master_config, + save_config, +) logger = logging.getLogger(__name__) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test logging -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_logging(): - logger.debug("Debug message") - logger.info("Info message") - logger.warning("Warn message") - logger.error("Error message") + logger.debug('Debug message') + logger.info('Info message') + logger.warning('Warn message') + logger.error('Error message') -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test config -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_phy_config_dir(): assert str(_config.phy_config_dir()).endswith('.phy') @@ -53,9 +54,10 @@ def test_temp_config_dir(temp_config_dir): assert _config.phy_config_dir() == temp_config_dir -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Config tests -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + @fixture def py_config(tempdir): @@ -93,7 +95,6 @@ def config(py_config, json_config, request): def test_load_config(config): - assert load_config() is not None class MyConfigurable(Configurable): @@ -128,13 +129,13 @@ def test_load_master_config_1(temp_config_dir): # Load the master config file. c = load_master_config() - assert c.MyConfigurable.my_var == 1. + assert c.MyConfigurable.my_var == 1.0 def test_save_config(tempdir): - c = {'A': {'b': 3.}} + c = {'A': {'b': 3.0}} path = tempdir / 'config.json' save_config(path, c) c1 = load_config(path) - assert c1.A.b == 3. + assert c1.A.b == 3.0 diff --git a/phy/utils/tests/test_context.py b/phy/utils/tests/test_context.py index ffb17af5e..49e54254a 100644 --- a/phy/utils/tests/test_context.py +++ b/phy/utils/tests/test_context.py @@ -1,28 +1,26 @@ -# -*- coding: utf-8 -*- - """Test context.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ from pickle import dump, load import numpy as np from numpy.testing import assert_array_equal as ae +from phylib.io.array import read_array, write_array from pytest import fixture -from phylib.io.array import write_array, read_array from ..context import Context, _fullname - -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Fixtures -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + @fixture(scope='function') def context(tempdir): - ctx = Context('{}/cache/'.format(tempdir), verbose=1) + ctx = Context(f'{tempdir}/cache/', verbose=1) return ctx @@ -30,15 +28,17 @@ def context(tempdir): def temp_phy_config_dir(tempdir): """Use a temporary phy user directory.""" import phy.utils.context + f = phy.utils.context.phy_config_dir phy.utils.context.phy_config_dir = lambda: tempdir yield phy.utils.context.phy_config_dir = f -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Test utils and cache -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_read_write(tempdir): x = np.arange(10) @@ -63,12 +63,11 @@ def test_context_load_save_pickle(tempdir, context, temp_phy_config_dir): def test_context_cache(context): - _res = [] def f(x): _res.append(x) - return x ** 2 + return x**2 x = np.arange(5) x2 = x * x @@ -88,7 +87,7 @@ def f(x): def test_context_cache_method(tempdir, context): - class A(object): + class A: def __init__(self, ctx): self.f = ctx.cache(self.f) self._l = [] @@ -109,7 +108,7 @@ def f(self, x): assert a._l == [3] # Recreate the context. - context = Context('{}/cache/'.format(tempdir), verbose=1) + context = Context(f'{tempdir}/cache/', verbose=1) # Recreate the class. a = A(context) assert a.f(3) == 3 @@ -118,21 +117,20 @@ def f(self, x): def test_context_memcache(tempdir, context): - _res = [] @context.memcache def f(x): _res.append(x) - return x ** 2 + return x**2 # Compute the function a first time. x = 10 - ae(f(x), x ** 2) + ae(f(x), x**2) assert len(_res) == 1 # The second time, the memory cache is used. - ae(f(x), x ** 2) + ae(f(x), x**2) assert len(_res) == 1 # We artificially clear the memory cache. @@ -141,7 +139,7 @@ def f(x): context.load_memcache(_fullname(f)) # This time, the result is loaded from disk. - ae(f(x), x ** 2) + ae(f(x), x**2) assert len(_res) == 1 diff --git a/phy/utils/tests/test_plugin.py b/phy/utils/tests/test_plugin.py index 7b5ee4e30..087c6b360 100644 --- a/phy/utils/tests/test_plugin.py +++ b/phy/utils/tests/test_plugin.py @@ -1,28 +1,21 @@ -# -*- coding: utf-8 -*- - """Test plugin system.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ from textwrap import dedent -from pytest import fixture, raises - -from ..plugin import (IPluginRegistry, - IPlugin, - get_plugin, - discover_plugins, - attach_plugins - ) from phylib.utils._misc import write_text +from pytest import fixture, raises +from ..plugin import IPlugin, IPluginRegistry, attach_plugins, discover_plugins, get_plugin -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Fixtures -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + @fixture def no_native_plugins(): @@ -33,9 +26,10 @@ def no_native_plugins(): IPluginRegistry.plugins = plugins -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Tests -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_plugin_1(no_native_plugins): class MyPlugin(IPlugin): @@ -50,7 +44,7 @@ class MyPlugin(IPlugin): def test_discover_plugins(tempdir, no_native_plugins): path = tempdir / 'my_plugin.py' - contents = '''from phy import IPlugin\nclass MyPlugin(IPlugin): pass''' + contents = """from phy import IPlugin\nclass MyPlugin(IPlugin): pass""" write_text(path, contents) plugins = discover_plugins([tempdir]) @@ -59,26 +53,30 @@ def test_discover_plugins(tempdir, no_native_plugins): def test_attach_plugins(tempdir): - class MyController(object): + class MyController: pass - write_text(tempdir / 'plugin1.py', dedent( - ''' + write_text( + tempdir / 'plugin1.py', + dedent( + """ from phy import IPlugin class MyPlugin1(IPlugin): def attach_to_controller(self, controller): controller.plugin1 = True - ''')) + """ + ), + ) class MyPlugin2(IPlugin): def attach_to_controller(self, controller): controller.plugin2 = True - contents = dedent(''' + contents = dedent(f""" c = get_config() - c.Plugins.dirs = ['%s'] + c.Plugins.dirs = ['{tempdir}'] c.MyController.plugins = ['MyPlugin1'] - ''' % tempdir) + """) write_text(tempdir / 'phy_config.py', contents) controller = MyController() diff --git a/phy/utils/tests/test_profiling.py b/phy/utils/tests/test_profiling.py index 191e3b64f..35ddad8b0 100644 --- a/phy/utils/tests/test_profiling.py +++ b/phy/utils/tests/test_profiling.py @@ -1,25 +1,23 @@ -# -*- coding: utf-8 -*- - """Tests of testing utility functions.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ import time from pytest import mark -from ..profiling import benchmark, _enable_profiler, _profile - +from ..profiling import _enable_profiler, _profile, benchmark -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Tests -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def test_benchmark(): with benchmark(): - time.sleep(.002) + time.sleep(0.002) @mark.parametrize('line_by_line', [False, True]) diff --git a/plugins/action_status_bar.py b/plugins/action_status_bar.py index 8b0f3e998..842e6ab37 100644 --- a/plugins/action_status_bar.py +++ b/plugins/action_status_bar.py @@ -14,7 +14,6 @@ class ExampleActionPlugin(IPlugin): def attach_to_controller(self, controller): @connect def on_gui_ready(sender, gui): - # Add a separator at the end of the File menu. # Note: currently, there is no way to add actions at another position in the menu. gui.file_actions.separator() @@ -27,7 +26,7 @@ def display_message(): # the menu item. # We update the text in the status bar. - gui.status_message = "Hello world" + gui.status_message = 'Hello world' # We add a separator at the end of the Select menu. gui.select_actions.separator() @@ -35,9 +34,9 @@ def display_message(): # Add an action to a new submenu called "My submenu". This action displays a prompt # dialog with the default value 10. @gui.select_actions.add( - submenu='My submenu', shortcut='ctrl+c', prompt=True, prompt_default=lambda: 10) + submenu='My submenu', shortcut='ctrl+c', prompt=True, prompt_default=lambda: 10 + ) def select_n_first_clusters(n_clusters): - # All cluster view methods are called with a callback function because of the # asynchronous nature of Python-Javascript interactions in Qt5. @controller.supervisor.cluster_view.get_ids diff --git a/plugins/cluster_metadata.py b/plugins/cluster_metadata.py index b2f6d0162..0e6fc0c4e 100644 --- a/plugins/cluster_metadata.py +++ b/plugins/cluster_metadata.py @@ -7,9 +7,10 @@ import logging -from phy import IPlugin, connect from phylib.io.model import save_metadata +from phy import IPlugin, connect + logger = logging.getLogger('phy') @@ -17,7 +18,6 @@ class ExampleClusterMetadataPlugin(IPlugin): def attach_to_controller(self, controller): @connect def on_gui_ready(sender, gui): - @connect(sender=gui) def on_request_save(sender): """This function is called whenever the Save action is triggered.""" @@ -43,8 +43,9 @@ def on_request_save(sender): # Dictionary mapping cluster_ids to the best channel id. metadata = { cluster_id: controller.get_best_channel(cluster_id) - for cluster_id in cluster_ids} + for cluster_id in cluster_ids + } # Save the metadata file. save_metadata(filename, field_name, metadata) - logger.info("Saved %s.", filename) + logger.info('Saved %s.', filename) diff --git a/plugins/cluster_metrics.py b/plugins/cluster_metrics.py index b824d67c4..afd7fdd5d 100644 --- a/plugins/cluster_metrics.py +++ b/plugins/cluster_metrics.py @@ -1,6 +1,7 @@ """Show how to add a custom cluster metrics.""" import numpy as np + from phy import IPlugin diff --git a/plugins/cluster_stats.py b/plugins/cluster_stats.py index a91f66a4f..62b126cc0 100644 --- a/plugins/cluster_stats.py +++ b/plugins/cluster_stats.py @@ -1,19 +1,19 @@ """Show how to add a custom cluster histogram view showing cluster statistics.""" -from phy import IPlugin, Bunch +from phy import Bunch, IPlugin from phy.cluster.views import HistogramView class FeatureHistogramView(HistogramView): """Every view corresponds to a unique view class, so we need to subclass HistogramView.""" + n_bins = 100 # default number of bins - x_max = .1 # maximum value on the x axis (maximum bin) + x_max = 0.1 # maximum value on the x axis (maximum bin) alias_char = 'fh' # provide `:fhn` (set number of bins) and `:fhm` (set max bin) snippets class ExampleClusterStatsPlugin(IPlugin): def attach_to_controller(self, controller): - def feature_histogram(cluster_id): """Must return a Bunch object with data and optional x_max, plot, text items. diff --git a/plugins/custom_button.py b/plugins/custom_button.py index ee58e14f7..34e59d86b 100644 --- a/plugins/custom_button.py +++ b/plugins/custom_button.py @@ -9,7 +9,6 @@ def attach_to_controller(self, controller): @connect def on_view_attached(view, gui): if isinstance(view, WaveformView): - # view.dock is a DockWidget instance, it has methods such as add_button(), # add_checkbox(), and set_status(). diff --git a/plugins/custom_similarity.py b/plugins/custom_similarity.py index a60928eff..5ed754c6b 100644 --- a/plugins/custom_similarity.py +++ b/plugins/custom_similarity.py @@ -1,6 +1,7 @@ """Show how to add a custom similarity measure.""" from operator import itemgetter + import numpy as np from phy import IPlugin @@ -17,8 +18,8 @@ def _dot_product(mw1, c1, mw2, c2): assert mw2.ndim == 2 # (n_samples, n_channels_loc_2) # We normalize the waveforms. - mw1 /= np.sqrt(np.sum(mw1 ** 2)) - mw2 /= np.sqrt(np.sum(mw2 ** 2)) + mw1 /= np.sqrt(np.sum(mw1**2)) + mw2 /= np.sqrt(np.sum(mw2**2)) # We find the union of the channel ids for both clusters so that we can convert from sparse # to dense format. @@ -42,7 +43,6 @@ def _dot_product(mw1, c1, mw2, c2): class ExampleSimilarityPlugin(IPlugin): def attach_to_controller(self, controller): - # We cache this function in memory and on disk. @controller.context.memcache def mean_waveform_similarity(cluster_id): diff --git a/plugins/custom_split.py b/plugins/custom_split.py index 19ef3ca1d..bf7c7367c 100644 --- a/plugins/custom_split.py +++ b/plugins/custom_split.py @@ -6,6 +6,7 @@ def k_means(x): """Cluster an array into two subclusters, using the K-means algorithm.""" from sklearn.cluster import KMeans + return KMeans(n_clusters=2).fit_predict(x) diff --git a/plugins/feature_view_custom_grid.py b/plugins/feature_view_custom_grid.py index 6d384b5f5..901e9c503 100644 --- a/plugins/feature_view_custom_grid.py +++ b/plugins/feature_view_custom_grid.py @@ -1,6 +1,7 @@ """Show how to customize the subplot grid specifiction in the feature view.""" import re + from phy import IPlugin, connect from phy.cluster.views import FeatureView diff --git a/plugins/filter_action.py b/plugins/filter_action.py index c015b94e7..0908b8461 100644 --- a/plugins/filter_action.py +++ b/plugins/filter_action.py @@ -14,4 +14,4 @@ def on_gui_ready(sender, gui): @gui.view_actions.add(alias='fr') # corresponds to `:fr` snippet def filter_firing_rate(rate): """Filter clusters with the firing rate.""" - controller.supervisor.filter('fr > %.1f' % float(rate)) + controller.supervisor.filter(f'fr > {float(rate):.1f}') diff --git a/plugins/font_size.py b/plugins/font_size.py index d1901d9e7..db3c3bcc4 100644 --- a/plugins/font_size.py +++ b/plugins/font_size.py @@ -7,4 +7,4 @@ class ExampleFontSizePlugin(IPlugin): def attach_to_controller(self, controller): # Smaller font size than the default (6). - TextVisual.default_font_size = 4. + TextVisual.default_font_size = 4.0 diff --git a/plugins/hello.py b/plugins/hello.py index 3ca7d596f..7dfae7386 100644 --- a/plugins/hello.py +++ b/plugins/hello.py @@ -15,4 +15,4 @@ def on_gui_ready(sender, gui): def on_cluster(sender, up): """This is called every time a cluster assignment or cluster group/label changes.""" - print("Clusters update: %s" % up) + print(f'Clusters update: {up}') diff --git a/plugins/matplotlib_view.py b/plugins/matplotlib_view.py index 71b4430fb..16852d4ec 100644 --- a/plugins/matplotlib_view.py +++ b/plugins/matplotlib_view.py @@ -10,7 +10,7 @@ class FeatureDensityView(ManualClusteringView): def __init__(self, features=None): """features is a function (cluster_id => Bunch(data, ...)) where data is a 3D array.""" - super(FeatureDensityView, self).__init__() + super().__init__() self.features = features def on_select(self, cluster_ids=(), **kwargs): diff --git a/plugins/opengl_view.py b/plugins/opengl_view.py index 23a5a6813..58e148e4d 100644 --- a/plugins/opengl_view.py +++ b/plugins/opengl_view.py @@ -2,11 +2,10 @@ import numpy as np -from phy.utils.color import selected_cluster_color - from phy import IPlugin from phy.cluster.views import ManualClusteringView from phy.plot.visuals import PlotVisual +from phy.utils.color import selected_cluster_color class MyOpenGLView(ManualClusteringView): @@ -19,7 +18,7 @@ def __init__(self, templates=None): the data as NumPy arrays. Many such functions are defined in the TemplateController. """ - super(MyOpenGLView, self).__init__() + super().__init__() """ The View instance contains a special `canvas` object which is a `̀PlotCanvas` instance. @@ -149,7 +148,7 @@ def on_select(self, cluster_ids=(), **kwargs): We decide to use, on the x axis, values ranging from -1 to 1. This is the standard viewport in OpenGL and phy. """ - x = np.linspace(-1., 1., len(y)) + x = np.linspace(-1.0, 1.0, len(y)) """ phy requires you to specify explicitly the x and y range of the plots. @@ -181,7 +180,8 @@ def on_select(self, cluster_ids=(), **kwargs): top to bottom. Note that in the grid view, the box index is a pair (row, col). """ self.visual.add_batch_data( - x=x, y=y, color=color, data_bounds=data_bounds, box_index=idx) + x=x, y=y, color=color, data_bounds=data_bounds, box_index=idx + ) """ After the loop, this special call automatically builds the data to upload to the GPU diff --git a/plugins/umap_view.py b/plugins/umap_view.py index 50beada4c..0b9011920 100644 --- a/plugins/umap_view.py +++ b/plugins/umap_view.py @@ -1,18 +1,18 @@ """Show how to write a custom dimension reduction view.""" -from phy import IPlugin, Bunch +from phy import Bunch, IPlugin from phy.cluster.views import ScatterView def umap(x): """Perform the dimension reduction of the array x.""" from umap import UMAP + return UMAP().fit_transform(x) class WaveformUMAPView(ScatterView): """Every view corresponds to a unique view class, so we need to subclass ScatterView.""" - pass class ExampleWaveformUMAPPlugin(IPlugin): diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..d781dcfa6 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,192 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "phy" +version = "2.0.0" # No dynamic lookup needed +description = "Interactive visualization and manual spike sorting of large-scale ephys data" +readme = "README.md" +license = { text = "BSD" } +authors = [ + { name = "Cyrille Rossant (cortex-lab/UCL/IBL)", email = "cyrille.rossant+pypi@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" + +# specific version required for pyopengl >=3.1.9 +dependencies = [ + "phylib @ git+https://github.com/jesusdpa1/phylib_update.git", + "click", + "colorcet", + "cython", + "dask", + "h5py", + "joblib", + "matplotlib", + "mtscomp", + "numba", + "numpy", + "pillow", + "pip", + "pyopengl>=3.1.9", + "qtconsole", + "requests", + "responses", + "scikit-learn", + "scipy", + "setuptools", + "tqdm", + "traitlets", + "ipykernel", +] + +[project.urls] +Homepage = "https://phy.cortexlab.net" +Repository = "https://github.com/cortex-lab/phy" +Documentation = "https://phy.cortexlab.net" + +[project.optional-dependencies] +dev = [ + "pytest", + "pytest-qt", + "pytest-cov", + "ruff", + "coverage", + "coveralls", + "memory_profiler", + "mkdocs", +] + +# 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"] + +# Convenience extras that install Qt6 by default +gui = ["pyqt6>=6.9.1", "pyqt6-webengine>=6.9.0"] + +# For users who want to specify Qt version explicitly +qt = [ + # This will be empty - users should choose qt5 or qt6 +] + +[project.scripts] +phy = "phy.apps:phycli" + +[tool.setuptools.dynamic] +version = { attr = "phy.version" } + +[tool.setuptools.packages.find] +include = ["phy*"] + +[tool.setuptools.package-data] +phy = [ + ".vert", + ".frag", + ".glsl", + ".npy", + ".gz", + ".txt", + ".json", + ".html", + ".css", + ".js", + ".prb", + ".ttf", + "*.png", +] + +[tool.pytest.inioptions] +testpaths = ["phy"] +addopts = "--ignore=phy/apps/kwik --cov=phy --cov-report=term-missing" +norecursedirs = ["experimental", ""] +filterwarnings = [ + "default", + "ignore::DeprecationWarning:.", + "ignore:numpy.ufunc", +] + +[tool.coverage.run] +branch = false +source = ["phy"] +omit = [ + "/phy/ext/", + "/phy/utils/tempdir.py", + "/default_settings.py", + "/phy/plot/gloo/", +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "raise AssertionError", + "raise NotImplementedError", + "pass", + "continue", + "qtbot.stop()", + "_in_travis():", + "_is_high_dpi():", + "return$", + "^\"\"\"", +] +omit = ["/phy/plot/gloo/"] +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", +] + +[tool.ruff.lint.isort] +known-first-party = ["phy"] + +[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/tools/api.py b/tools/api.py index 3d38683d2..12e90e96e 100644 --- a/tools/api.py +++ b/tools/api.py @@ -1,19 +1,18 @@ -# -*- coding: utf-8 -*- """Minimal API documentation generation.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ -from importlib import import_module import inspect import os.path as op import re +from importlib import import_module - -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Utility functions -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _name(obj): if hasattr(obj, '__name__'): @@ -23,7 +22,7 @@ def _name(obj): def _full_name(subpackage, obj): - return '{}.{}'.format(subpackage.__name__, _name(obj)) + return f'{subpackage.__name__}.{_name(obj)}' def _anchor(name): @@ -56,9 +55,10 @@ def _doc(obj): return doc -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Introspection methods -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _is_public(obj): name = _name(obj) if not isinstance(obj, str) else obj @@ -88,7 +88,7 @@ def _iter_doc_members(obj, package=None): def _iter_subpackages(package, subpackages): """Iterate through a list of subpackages.""" for subpackage in subpackages: - yield import_module('{}.{}'.format(package, subpackage)) + yield import_module(f'{package}.{subpackage}') def _iter_vars(mod): @@ -120,14 +120,15 @@ def _iter_properties(klass, package=None): yield member.fget -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # API doc generation -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + def _function_header(subpackage, func): """Generate the docstring of a function.""" args = str(inspect.signature(func)) - return "{name}{args}".format(name=_full_name(subpackage, func), args=args) + return f'{_full_name(subpackage, func)}{args}' _FUNCTION_PATTERN = '%s\n\n\n**`%s`**\n\n%s\n\n---' @@ -141,51 +142,48 @@ def _doc_function(subpackage, func): def _doc_method(klass, func): """Generate the docstring of a method.""" args = str(inspect.signature(func)) - title = "{klass}.{name}".format(klass=klass.__name__, name=_name(func)) - header = "{klass}.{name}{args}".format(klass=klass.__name__, name=_name(func), args=args) + title = f'{klass.__name__}.{_name(func)}' + header = f'{klass.__name__}.{_name(func)}{args}' docstring = _doc(func) return _FUNCTION_PATTERN % (title, header, docstring) def _doc_property(klass, prop): """Generate the docstring of a property.""" - header = "{klass}.{name}".format(klass=klass.__name__, name=_name(prop)) + header = f'{klass.__name__}.{_name(prop)}' docstring = _doc(prop) return _FUNCTION_PATTERN % (header, header, docstring) def _link(name, anchor=None): - return "[{name}](#{anchor})".format(name=name, anchor=anchor or _anchor(name)) + return f'[{name}](#{anchor or _anchor(name)})' def _generate_preamble(package, subpackages): - - yield "# API documentation of {}".format(package) + yield f'# API documentation of {package}' yield _doc(import_module(package)) - yield "## Table of contents" + yield '## Table of contents' # Table of contents: list of modules. for subpackage in _iter_subpackages(package, subpackages): subpackage_name = subpackage.__name__ - yield "### " + _link(subpackage_name) + yield f'### {_link(subpackage_name)}' # List of top-level functions in the subpackage. for func in _iter_functions(subpackage): - yield '* ' + _link( - _full_name(subpackage, func), _anchor(_full_name(subpackage, func))) + yield f'* {_link(_full_name(subpackage, func), _anchor(_full_name(subpackage, func)))}' # All public classes. for klass in _iter_classes(subpackage): - # Class documentation. - yield "* " + _link(_full_name(subpackage, klass)) + yield f'* {_link(_full_name(subpackage, klass))}' - yield "" + yield '' - yield "" + yield '' def _generate_paragraphs(package, subpackages): @@ -195,36 +193,35 @@ def _generate_paragraphs(package, subpackages): for subpackage in _iter_subpackages(package, subpackages): subpackage_name = subpackage.__name__ - yield "## {}".format(subpackage_name) + yield f'## {subpackage_name}' # Subpackage documentation. yield _doc(import_module(subpackage_name)) - yield "---" + yield '---' # List of top-level functions in the subpackage. for func in _iter_functions(subpackage): - yield '#### ' + _doc_function(subpackage, func) + yield f'#### {_doc_function(subpackage, func)}' # All public classes. for klass in _iter_classes(subpackage): - # Class documentation. - yield "### {}".format(_full_name(subpackage, klass)) + yield f'### {_full_name(subpackage, klass)}' yield _doc(klass) - yield "---" + yield '---' for method in _iter_methods(klass, package): - yield '#### ' + _doc_method(klass, method) + yield f'#### {_doc_method(klass, method)}' for prop in _iter_properties(klass, package): - yield '#### ' + _doc_property(klass, prop) + yield f'#### {_doc_property(klass, prop)}' def _print_paragraph(paragraph): out = '' - out += paragraph + '\n' + out += f'{paragraph}\n' if not paragraph.startswith('* '): out += '\n' return out @@ -244,7 +241,6 @@ def generate_api_doc(package, subpackages, path=None): if __name__ == '__main__': - package = 'phy' subpackages = ['utils', 'gui', 'plot', 'cluster', 'apps', 'apps.template', 'apps.kwik'] diff --git a/tools/extract_shortcuts.py b/tools/extract_shortcuts.py index b9ce0bfda..43a661984 100644 --- a/tools/extract_shortcuts.py +++ b/tools/extract_shortcuts.py @@ -1,13 +1,13 @@ -from pathlib import Path import re +from pathlib import Path + +from phylib.utils.testing import captured_output from phy.apps.base import BaseController from phy.cluster import views from phy.cluster.supervisor import ActionCreator -from phy.gui.actions import _show_shortcuts, _show_snippets from phy.gui import GUI -from phylib.utils.testing import captured_output - +from phy.gui.actions import _show_shortcuts, _show_snippets # Get a mapping view class : list of keyboard shortcuts @@ -29,7 +29,7 @@ def _get_shortcuts(cls): for cls in view_classes: s = _get_shortcuts(cls) if '-' in s: - s = s[s.index('-') - 1:] + s = s[s.index('-') - 1 :] view_shortcuts[cls.__name__] = s @@ -46,7 +46,7 @@ def _get_shortcuts(cls): j = m.end(2) contents = contents[:i] + shortcuts + contents[j:] file.write_text(contents) - print("Inserted shortcuts for %s in %s." % (view_name, file)) + print(f'Inserted shortcuts for {view_name} in {file}.') # All shortcuts @@ -55,8 +55,11 @@ def _get_shortcuts(cls): gui_shortcuts = _get_shortcuts(GUI) all_shortcuts = ( - supervisor_shortcuts + base_shortcuts + gui_shortcuts + - ''.join(_get_shortcuts(cls) for cls in view_classes)) + supervisor_shortcuts + + base_shortcuts + + gui_shortcuts + + ''.join(_get_shortcuts(cls) for cls in view_classes) +) pattern = re.compile(r'```text\nAll keyboard shortcuts\n\n([^`]+)\n```') shortcuts_file = docs_dir / 'shortcuts.md' @@ -66,4 +69,4 @@ def _get_shortcuts(cls): j = m.end(1) contents = contents[:i] + all_shortcuts + contents[j:] shortcuts_file.write_text(contents) -print("Inserted all shortcuts in %s." % shortcuts_file) +print(f'Inserted all shortcuts in {shortcuts_file}.') diff --git a/tools/makefont.py b/tools/makefont.py index 3d473252c..2df9fb2c9 100644 --- a/tools/makefont.py +++ b/tools/makefont.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """Create a multi-channel signed distance field map. Use https://github.com/Chlumsky/msdfgen/ @@ -13,7 +12,6 @@ """ - import gzip import os from pathlib import Path @@ -21,13 +19,12 @@ import imageio import numpy as np -from phy.plot.visuals import FONT_MAP_SIZE, FONT_MAP_PATH, SDF_SIZE, FONT_MAP_CHARS, GLYPH_SIZE +from phy.plot.visuals import FONT_MAP_CHARS, FONT_MAP_PATH, FONT_MAP_SIZE, GLYPH_SIZE, SDF_SIZE -class FontMapGenerator(object): - """Generate a SDF font map for a monospace font, with a given uniform glyph size. +class FontMapGenerator: + """Generate a SDF font map for a monospace font, with a given uniform glyph size.""" - """ def __init__(self): self.rows, self.cols = FONT_MAP_SIZE self.font_map_output = FONT_MAP_PATH @@ -56,7 +53,8 @@ def _get_cmd(self, char_number): return ( f'{self.msdfgen_path} msdf -font {self.font} {char_number} -o {self.glyph_output} ' f'-size {self.width} {self.height} ' - '-pxrange 4 -scale 3.9 -translate 0.5 4') + '-pxrange 4 -scale 3.9 -translate 0.5 4' + ) def _get_glyph_array(self, char_number): """Return the NumPy array with a glyph, by calling the msdfgen tool.""" diff --git a/tools/plugins_doc.py b/tools/plugins_doc.py index 614b63963..5b6a36a30 100644 --- a/tools/plugins_doc.py +++ b/tools/plugins_doc.py @@ -1,11 +1,10 @@ -# -*- coding: utf-8 -*- """Import plugin code from `plugins/` files into the Markdown documentation file `plugins.md`.""" import ast import difflib +import re from pathlib import Path from pprint import pprint -import re def is_valid_python(code): @@ -35,7 +34,7 @@ def is_valid_python(code): plugins_doc = plugins_doc[:i] + plugin_contents + plugins_doc[j:] # Update the README. - title = class_name_pattern.search(plugin_contents).group(1) + 'Plugin' + title = f'{class_name_pattern.search(plugin_contents).group(1)}Plugin' desc = plugin_contents.splitlines()[0].replace('"', '') url = filename.name readme.append(f'* [{title}]({url}): {desc}') @@ -43,7 +42,7 @@ def is_valid_python(code): readme = sorted(readme) # Update the plugin README -(root_dir / 'plugins/README.md').write_text('# phy plugin examples\n\n' + '\n'.join(readme) + '\n') +(root_dir / 'plugins/README.md').write_text(f'# phy plugin examples\n\n{"\\n".join(readme)}\n') # Make sure the copied and pasted code in the Markdown file is correct. @@ -54,9 +53,9 @@ def is_valid_python(code): assert plugin_contents.strip() == m.group(2).strip() -print("DIFF\n----\n") +print('DIFF\n----\n') a, b = plugins_doc0.splitlines(), plugins_doc.splitlines() pprint('\n'.join([li for li in difflib.ndiff(a, b) if li[0] != ' '])) plugins_file.write_text(plugins_doc) -print("Updated doc.") +print('Updated doc.') diff --git a/tools/release.py b/tools/release.py index 5e1e308f5..d06050728 100644 --- a/tools/release.py +++ b/tools/release.py @@ -1,31 +1,25 @@ -# -*- coding: utf-8 -*- -from __future__ import print_function - """Automatic release tools.""" -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Imports -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ -import sys import os import os.path as op import re +import sys from subprocess import call from github3 import login - # ----------------------------------------------------------------------------- # Utilities # ----------------------------------------------------------------------------- + def _call(cmd, system=False): - if system: - ret = os.system(cmd) - else: - ret = call(cmd.split(' ')) + ret = os.system(cmd) if system else call(cmd.split(' ')) if ret != 0: raise RuntimeError() @@ -45,7 +39,7 @@ def _path(fn): def _get_stable_version(): fn = _path('phy/__init__.py') - with open(fn, 'r') as f: + with open(fn) as f: contents = f.read() m = re.search(_version_pattern, contents) return m.group(1) @@ -66,7 +60,7 @@ def func(m): raise ValueError() return _version_replace.format(m.group(1), dev, n) - with open(fn, 'r') as f: + with open(fn) as f: contents = f.read() contents_new = re.sub(_version_pattern, func, contents) @@ -91,35 +85,37 @@ def _set_final_version(): # Git[hub] tools # ----------------------------------------------------------------------------- + def _create_gh_release(): version = _get_stable_version() - name = 'Version {}'.format(version) - path = _path('dist/phy-{}.zip'.format(version)) + name = f'Version {version}' + path = _path(f'dist/phy-{version}.zip') assert op.exists(path) - with open(_path('.github_credentials'), 'r') as f: + with open(_path('.github_credentials')) as f: user, pwd = f.read().strip().split(':') gh = login(user, pwd) phy = gh.repository('kwikteam', 'phy') - if input("About to create a GitHub release: are you sure?") != 'yes': + if input('About to create a GitHub release: are you sure?') != 'yes': return - release = phy.create_release('v' + version, - name=name, - # draft=False, - # prerelease=False, - ) + release = phy.create_release( + f'v{version}', + name=name, + # draft=False, + # prerelease=False, + ) release.upload_asset('application/zip', op.basename(path), path) def _git_commit(message, push=False): assert message - if input("About to git commit {}: are you sure?") != 'yes': + if input('About to git commit {}: are you sure?') != 'yes': return - _call('git commit -am "{}"'.format(message)) + _call(f'git commit -am "{message}"') if push: - if input("About to git push upstream master: are you sure?") != 'yes': + if input('About to git push upstream master: are you sure?') != 'yes': return _call('git push upstream master') @@ -128,6 +124,7 @@ def _git_commit(message, push=False): # PyPI # ----------------------------------------------------------------------------- + def _upload_pypi(): _call('python setup.py sdist --formats=zip upload') @@ -136,23 +133,27 @@ def _upload_pypi(): # Docker # ----------------------------------------------------------------------------- + def _build_docker(): _call('docker build -t phy-release-test docker/stable') def _test_docker(): - _call('docker run --rm phy-release-test /sbin/start-stop-daemon --start ' - '--quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile ' - '--background --exec /usr/bin/Xvfb -- :99 -screen 0 1400x900x24 ' - '-ac +extension GLX +render && ' - 'python -c "import phy; phy.test()"', - system=True) + _call( + 'docker run --rm phy-release-test /sbin/start-stop-daemon --start ' + '--quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile ' + '--background --exec /usr/bin/Xvfb -- :99 -screen 0 1400x900x24 ' + '-ac +extension GLX +render && ' + 'python -c "import phy; phy.test()"', + system=True, + ) # ----------------------------------------------------------------------------- # Release functions # ----------------------------------------------------------------------------- + def release_test(): _increment_dev_version() _upload_pypi() @@ -164,7 +165,7 @@ def release(): version = _get_stable_version() _set_final_version() _upload_pypi() - _git_commit("Release {}.".format(version), push=True) + _git_commit(f'Release {version}.', push=True) _create_gh_release() diff --git a/uv.lock b/uv.lock new file mode 100644 index 000000000..22fe3197f --- /dev/null +++ b/uv.lock @@ -0,0 +1,3013 @@ +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 = "appnope" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, +] + +[[package]] +name = "asttokens" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978, upload-time = "2024-11-30T04:30:14.439Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918, upload-time = "2024-11-30T04:30:10.946Z" }, +] + +[[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 = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191, upload-time = "2024-09-04T20:43:30.027Z" }, + { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592, upload-time = "2024-09-04T20:43:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804, upload-time = "2024-09-04T20:43:48.186Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299, upload-time = "2024-09-04T20:43:49.812Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ea/8bb50596b8ffbc49ddd7a1ad305035daa770202a6b782fc164647c2673ad/cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", size = 182220, upload-time = "2024-09-04T20:45:01.577Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/e77c8cd24f58285a82c23af484cf5b124a376b32644e445960d1a4654c3a/cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", size = 178605, upload-time = "2024-09-04T20:45:03.837Z" }, + { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910, upload-time = "2024-09-04T20:45:05.315Z" }, + { url = "https://files.pythonhosted.org/packages/42/7a/9d086fab7c66bd7c4d0f27c57a1b6b068ced810afc498cc8c49e0088661c/cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", size = 447200, upload-time = "2024-09-04T20:45:06.903Z" }, + { url = "https://files.pythonhosted.org/packages/da/63/1785ced118ce92a993b0ec9e0d0ac8dc3e5dbfbcaa81135be56c69cabbb6/cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", size = 454565, upload-time = "2024-09-04T20:45:08.975Z" }, + { url = "https://files.pythonhosted.org/packages/74/06/90b8a44abf3556599cdec107f7290277ae8901a58f75e6fe8f970cd72418/cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", size = 435635, upload-time = "2024-09-04T20:45:10.64Z" }, + { url = "https://files.pythonhosted.org/packages/bd/62/a1f468e5708a70b1d86ead5bab5520861d9c7eacce4a885ded9faa7729c3/cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", size = 445218, upload-time = "2024-09-04T20:45:12.366Z" }, + { url = "https://files.pythonhosted.org/packages/5b/95/b34462f3ccb09c2594aa782d90a90b045de4ff1f70148ee79c69d37a0a5a/cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", size = 460486, upload-time = "2024-09-04T20:45:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/fc/fc/a1e4bebd8d680febd29cf6c8a40067182b64f00c7d105f8f26b5bc54317b/cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", size = 437911, upload-time = "2024-09-04T20:45:15.696Z" }, + { url = "https://files.pythonhosted.org/packages/e6/c3/21cab7a6154b6a5ea330ae80de386e7665254835b9e98ecc1340b3a7de9a/cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", size = 460632, upload-time = "2024-09-04T20:45:17.284Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b5/fd9f8b5a84010ca169ee49f4e4ad6f8c05f4e3545b72ee041dbbcb159882/cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", size = 171820, upload-time = "2024-09-04T20:45:18.762Z" }, + { url = "https://files.pythonhosted.org/packages/8c/52/b08750ce0bce45c143e1b5d7357ee8c55341b52bdef4b0f081af1eb248c2/cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", size = 181290, upload-time = "2024-09-04T20:45:20.226Z" }, +] + +[[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 = "colorcet" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/c3/ae78e10b7139d6b7ce080d2e81d822715763336aa4229720f49cb3b3e15b/colorcet-3.1.0.tar.gz", hash = "sha256:2921b3cd81a2288aaf2d63dbc0ce3c26dcd882e8c389cc505d6886bf7aa9a4eb", size = 2183107, upload-time = "2024-02-29T19:15:42.976Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/c6/9963d588cc3d75d766c819e0377a168ef83cf3316a92769971527a1ad1de/colorcet-3.1.0-py3-none-any.whl", hash = "sha256:2a7d59cc8d0f7938eeedd08aad3152b5319b4ba3bcb7a612398cc17a384cb296", size = 260286, upload-time = "2024-02-29T19:15:40.494Z" }, +] + +[[package]] +name = "comm" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/a8/fb783cb0abe2b5fded9f55e5703015cdf1c9c85b3669087c538dd15a6a86/comm-0.2.2.tar.gz", hash = "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e", size = 6210, upload-time = "2024-03-12T16:53:41.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/75/49e5bfe642f71f272236b5b2d2691cf915a7283cc0ceda56357b61daa538/comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3", size = 7180, upload-time = "2024-03-12T16:53:39.226Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.0" +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/f5/f6/31a8f28b4a2a4fa0e01085e542f3081ab0588eff8e589d39d775172c9792/contourpy-1.3.0.tar.gz", hash = "sha256:7ffa0db17717a8ffb127efd0c95a4362d996b892c2904db72428d5b52e1938a4", size = 13464370, upload-time = "2024-08-27T21:00:03.328Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/e0/be8dcc796cfdd96708933e0e2da99ba4bb8f9b2caa9d560a50f3f09a65f3/contourpy-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:880ea32e5c774634f9fcd46504bf9f080a41ad855f4fef54f5380f5133d343c7", size = 265366, upload-time = "2024-08-27T20:50:09.947Z" }, + { url = "https://files.pythonhosted.org/packages/50/d6/c953b400219443535d412fcbbc42e7a5e823291236bc0bb88936e3cc9317/contourpy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:76c905ef940a4474a6289c71d53122a4f77766eef23c03cd57016ce19d0f7b42", size = 249226, upload-time = "2024-08-27T20:50:16.1Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b4/6fffdf213ffccc28483c524b9dad46bb78332851133b36ad354b856ddc7c/contourpy-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92f8557cbb07415a4d6fa191f20fd9d2d9eb9c0b61d1b2f52a8926e43c6e9af7", size = 308460, upload-time = "2024-08-27T20:50:22.536Z" }, + { url = "https://files.pythonhosted.org/packages/cf/6c/118fc917b4050f0afe07179a6dcbe4f3f4ec69b94f36c9e128c4af480fb8/contourpy-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36f965570cff02b874773c49bfe85562b47030805d7d8360748f3eca570f4cab", size = 347623, upload-time = "2024-08-27T20:50:28.806Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a4/30ff110a81bfe3abf7b9673284d21ddce8cc1278f6f77393c91199da4c90/contourpy-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cacd81e2d4b6f89c9f8a5b69b86490152ff39afc58a95af002a398273e5ce589", size = 317761, upload-time = "2024-08-27T20:50:35.126Z" }, + { url = "https://files.pythonhosted.org/packages/99/e6/d11966962b1aa515f5586d3907ad019f4b812c04e4546cc19ebf62b5178e/contourpy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69375194457ad0fad3a839b9e29aa0b0ed53bb54db1bfb6c3ae43d111c31ce41", size = 322015, upload-time = "2024-08-27T20:50:40.318Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e3/182383743751d22b7b59c3c753277b6aee3637049197624f333dac5b4c80/contourpy-1.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a52040312b1a858b5e31ef28c2e865376a386c60c0e248370bbea2d3f3b760d", size = 1262672, upload-time = "2024-08-27T20:50:55.643Z" }, + { url = "https://files.pythonhosted.org/packages/78/53/974400c815b2e605f252c8fb9297e2204347d1755a5374354ee77b1ea259/contourpy-1.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3faeb2998e4fcb256542e8a926d08da08977f7f5e62cf733f3c211c2a5586223", size = 1321688, upload-time = "2024-08-27T20:51:11.293Z" }, + { url = "https://files.pythonhosted.org/packages/52/29/99f849faed5593b2926a68a31882af98afbeac39c7fdf7de491d9c85ec6a/contourpy-1.3.0-cp310-cp310-win32.whl", hash = "sha256:36e0cff201bcb17a0a8ecc7f454fe078437fa6bda730e695a92f2d9932bd507f", size = 171145, upload-time = "2024-08-27T20:51:15.2Z" }, + { url = "https://files.pythonhosted.org/packages/a9/97/3f89bba79ff6ff2b07a3cbc40aa693c360d5efa90d66e914f0ff03b95ec7/contourpy-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:87ddffef1dbe5e669b5c2440b643d3fdd8622a348fe1983fad7a0f0ccb1cd67b", size = 216019, upload-time = "2024-08-27T20:51:19.365Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1f/9375917786cb39270b0ee6634536c0e22abf225825602688990d8f5c6c19/contourpy-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fa4c02abe6c446ba70d96ece336e621efa4aecae43eaa9b030ae5fb92b309ad", size = 266356, upload-time = "2024-08-27T20:51:24.146Z" }, + { url = "https://files.pythonhosted.org/packages/05/46/9256dd162ea52790c127cb58cfc3b9e3413a6e3478917d1f811d420772ec/contourpy-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:834e0cfe17ba12f79963861e0f908556b2cedd52e1f75e6578801febcc6a9f49", size = 250915, upload-time = "2024-08-27T20:51:28.683Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5d/3056c167fa4486900dfbd7e26a2fdc2338dc58eee36d490a0ed3ddda5ded/contourpy-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbc4c3217eee163fa3984fd1567632b48d6dfd29216da3ded3d7b844a8014a66", size = 310443, upload-time = "2024-08-27T20:51:33.675Z" }, + { url = "https://files.pythonhosted.org/packages/ca/c2/1a612e475492e07f11c8e267ea5ec1ce0d89971be496c195e27afa97e14a/contourpy-1.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4865cd1d419e0c7a7bf6de1777b185eebdc51470800a9f42b9e9decf17762081", size = 348548, upload-time = "2024-08-27T20:51:39.322Z" }, + { url = "https://files.pythonhosted.org/packages/45/cf/2c2fc6bb5874158277b4faf136847f0689e1b1a1f640a36d76d52e78907c/contourpy-1.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:303c252947ab4b14c08afeb52375b26781ccd6a5ccd81abcdfc1fafd14cf93c1", size = 319118, upload-time = "2024-08-27T20:51:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/03/33/003065374f38894cdf1040cef474ad0546368eea7e3a51d48b8a423961f8/contourpy-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637f674226be46f6ba372fd29d9523dd977a291f66ab2a74fbeb5530bb3f445d", size = 323162, upload-time = "2024-08-27T20:51:49.683Z" }, + { url = "https://files.pythonhosted.org/packages/42/80/e637326e85e4105a802e42959f56cff2cd39a6b5ef68d5d9aee3ea5f0e4c/contourpy-1.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:76a896b2f195b57db25d6b44e7e03f221d32fe318d03ede41f8b4d9ba1bff53c", size = 1265396, upload-time = "2024-08-27T20:52:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/7c/3b/8cbd6416ca1bbc0202b50f9c13b2e0b922b64be888f9d9ee88e6cfabfb51/contourpy-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e1fd23e9d01591bab45546c089ae89d926917a66dceb3abcf01f6105d927e2cb", size = 1324297, upload-time = "2024-08-27T20:52:21.843Z" }, + { url = "https://files.pythonhosted.org/packages/4d/2c/021a7afaa52fe891f25535506cc861c30c3c4e5a1c1ce94215e04b293e72/contourpy-1.3.0-cp311-cp311-win32.whl", hash = "sha256:d402880b84df3bec6eab53cd0cf802cae6a2ef9537e70cf75e91618a3801c20c", size = 171808, upload-time = "2024-08-27T20:52:25.163Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2f/804f02ff30a7fae21f98198828d0857439ec4c91a96e20cf2d6c49372966/contourpy-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:6cb6cc968059db9c62cb35fbf70248f40994dfcd7aa10444bbf8b3faeb7c2d67", size = 217181, upload-time = "2024-08-27T20:52:29.13Z" }, + { url = "https://files.pythonhosted.org/packages/c9/92/8e0bbfe6b70c0e2d3d81272b58c98ac69ff1a4329f18c73bd64824d8b12e/contourpy-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:570ef7cf892f0afbe5b2ee410c507ce12e15a5fa91017a0009f79f7d93a1268f", size = 267838, upload-time = "2024-08-27T20:52:33.911Z" }, + { url = "https://files.pythonhosted.org/packages/e3/04/33351c5d5108460a8ce6d512307690b023f0cfcad5899499f5c83b9d63b1/contourpy-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:da84c537cb8b97d153e9fb208c221c45605f73147bd4cadd23bdae915042aad6", size = 251549, upload-time = "2024-08-27T20:52:39.179Z" }, + { url = "https://files.pythonhosted.org/packages/51/3d/aa0fe6ae67e3ef9f178389e4caaaa68daf2f9024092aa3c6032e3d174670/contourpy-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0be4d8425bfa755e0fd76ee1e019636ccc7c29f77a7c86b4328a9eb6a26d0639", size = 303177, upload-time = "2024-08-27T20:52:44.789Z" }, + { url = "https://files.pythonhosted.org/packages/56/c3/c85a7e3e0cab635575d3b657f9535443a6f5d20fac1a1911eaa4bbe1aceb/contourpy-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c0da700bf58f6e0b65312d0a5e695179a71d0163957fa381bb3c1f72972537c", size = 341735, upload-time = "2024-08-27T20:52:51.05Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8d/20f7a211a7be966a53f474bc90b1a8202e9844b3f1ef85f3ae45a77151ee/contourpy-1.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb8b141bb00fa977d9122636b16aa67d37fd40a3d8b52dd837e536d64b9a4d06", size = 314679, upload-time = "2024-08-27T20:52:58.473Z" }, + { url = "https://files.pythonhosted.org/packages/6e/be/524e377567defac0e21a46e2a529652d165fed130a0d8a863219303cee18/contourpy-1.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3634b5385c6716c258d0419c46d05c8aa7dc8cb70326c9a4fb66b69ad2b52e09", size = 320549, upload-time = "2024-08-27T20:53:06.593Z" }, + { url = "https://files.pythonhosted.org/packages/0f/96/fdb2552a172942d888915f3a6663812e9bc3d359d53dafd4289a0fb462f0/contourpy-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0dce35502151b6bd35027ac39ba6e5a44be13a68f55735c3612c568cac3805fd", size = 1263068, upload-time = "2024-08-27T20:53:23.442Z" }, + { url = "https://files.pythonhosted.org/packages/2a/25/632eab595e3140adfa92f1322bf8915f68c932bac468e89eae9974cf1c00/contourpy-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea348f053c645100612b333adc5983d87be69acdc6d77d3169c090d3b01dc35", size = 1322833, upload-time = "2024-08-27T20:53:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/73/e3/69738782e315a1d26d29d71a550dbbe3eb6c653b028b150f70c1a5f4f229/contourpy-1.3.0-cp312-cp312-win32.whl", hash = "sha256:90f73a5116ad1ba7174341ef3ea5c3150ddf20b024b98fb0c3b29034752c8aeb", size = 172681, upload-time = "2024-08-27T20:53:43.05Z" }, + { url = "https://files.pythonhosted.org/packages/0c/89/9830ba00d88e43d15e53d64931e66b8792b46eb25e2050a88fec4a0df3d5/contourpy-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b11b39aea6be6764f84360fce6c82211a9db32a7c7de8fa6dd5397cf1d079c3b", size = 218283, upload-time = "2024-08-27T20:53:47.232Z" }, + { url = "https://files.pythonhosted.org/packages/53/a1/d20415febfb2267af2d7f06338e82171824d08614084714fb2c1dac9901f/contourpy-1.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3e1c7fa44aaae40a2247e2e8e0627f4bea3dd257014764aa644f319a5f8600e3", size = 267879, upload-time = "2024-08-27T20:53:51.597Z" }, + { url = "https://files.pythonhosted.org/packages/aa/45/5a28a3570ff6218d8bdfc291a272a20d2648104815f01f0177d103d985e1/contourpy-1.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:364174c2a76057feef647c802652f00953b575723062560498dc7930fc9b1cb7", size = 251573, upload-time = "2024-08-27T20:53:55.659Z" }, + { url = "https://files.pythonhosted.org/packages/39/1c/d3f51540108e3affa84f095c8b04f0aa833bb797bc8baa218a952a98117d/contourpy-1.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32b238b3b3b649e09ce9aaf51f0c261d38644bdfa35cbaf7b263457850957a84", size = 303184, upload-time = "2024-08-27T20:54:00.225Z" }, + { url = "https://files.pythonhosted.org/packages/00/56/1348a44fb6c3a558c1a3a0cd23d329d604c99d81bf5a4b58c6b71aab328f/contourpy-1.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d51fca85f9f7ad0b65b4b9fe800406d0d77017d7270d31ec3fb1cc07358fdea0", size = 340262, upload-time = "2024-08-27T20:54:05.234Z" }, + { url = "https://files.pythonhosted.org/packages/2b/23/00d665ba67e1bb666152131da07e0f24c95c3632d7722caa97fb61470eca/contourpy-1.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:732896af21716b29ab3e988d4ce14bc5133733b85956316fb0c56355f398099b", size = 313806, upload-time = "2024-08-27T20:54:09.889Z" }, + { url = "https://files.pythonhosted.org/packages/5a/42/3cf40f7040bb8362aea19af9a5fb7b32ce420f645dd1590edcee2c657cd5/contourpy-1.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d73f659398a0904e125280836ae6f88ba9b178b2fed6884f3b1f95b989d2c8da", size = 319710, upload-time = "2024-08-27T20:54:14.536Z" }, + { url = "https://files.pythonhosted.org/packages/05/32/f3bfa3fc083b25e1a7ae09197f897476ee68e7386e10404bdf9aac7391f0/contourpy-1.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c6c7c2408b7048082932cf4e641fa3b8ca848259212f51c8c59c45aa7ac18f14", size = 1264107, upload-time = "2024-08-27T20:54:29.735Z" }, + { url = "https://files.pythonhosted.org/packages/1c/1e/1019d34473a736664f2439542b890b2dc4c6245f5c0d8cdfc0ccc2cab80c/contourpy-1.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f317576606de89da6b7e0861cf6061f6146ead3528acabff9236458a6ba467f8", size = 1322458, upload-time = "2024-08-27T20:54:45.507Z" }, + { url = "https://files.pythonhosted.org/packages/22/85/4f8bfd83972cf8909a4d36d16b177f7b8bdd942178ea4bf877d4a380a91c/contourpy-1.3.0-cp313-cp313-win32.whl", hash = "sha256:31cd3a85dbdf1fc002280c65caa7e2b5f65e4a973fcdf70dd2fdcb9868069294", size = 172643, upload-time = "2024-08-27T20:55:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4a/fb3c83c1baba64ba90443626c228ca14f19a87c51975d3b1de308dd2cf08/contourpy-1.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4553c421929ec95fb07b3aaca0fae668b2eb5a5203d1217ca7c34c063c53d087", size = 218301, upload-time = "2024-08-27T20:55:56.509Z" }, + { url = "https://files.pythonhosted.org/packages/76/65/702f4064f397821fea0cb493f7d3bc95a5d703e20954dce7d6d39bacf378/contourpy-1.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:345af746d7766821d05d72cb8f3845dfd08dd137101a2cb9b24de277d716def8", size = 278972, upload-time = "2024-08-27T20:54:50.347Z" }, + { url = "https://files.pythonhosted.org/packages/80/85/21f5bba56dba75c10a45ec00ad3b8190dbac7fd9a8a8c46c6116c933e9cf/contourpy-1.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3bb3808858a9dc68f6f03d319acd5f1b8a337e6cdda197f02f4b8ff67ad2057b", size = 263375, upload-time = "2024-08-27T20:54:54.909Z" }, + { url = "https://files.pythonhosted.org/packages/0a/64/084c86ab71d43149f91ab3a4054ccf18565f0a8af36abfa92b1467813ed6/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:420d39daa61aab1221567b42eecb01112908b2cab7f1b4106a52caaec8d36973", size = 307188, upload-time = "2024-08-27T20:55:00.184Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/d61a4c288dc42da0084b8d9dc2aa219a850767165d7d9a9c364ff530b509/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d63ee447261e963af02642ffcb864e5a2ee4cbfd78080657a9880b8b1868e18", size = 345644, upload-time = "2024-08-27T20:55:05.673Z" }, + { url = "https://files.pythonhosted.org/packages/ca/aa/00d2313d35ec03f188e8f0786c2fc61f589306e02fdc158233697546fd58/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:167d6c890815e1dac9536dca00828b445d5d0df4d6a8c6adb4a7ec3166812fa8", size = 317141, upload-time = "2024-08-27T20:55:11.047Z" }, + { url = "https://files.pythonhosted.org/packages/8d/6a/b5242c8cb32d87f6abf4f5e3044ca397cb1a76712e3fa2424772e3ff495f/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:710a26b3dc80c0e4febf04555de66f5fd17e9cf7170a7b08000601a10570bda6", size = 323469, upload-time = "2024-08-27T20:55:15.914Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a6/73e929d43028a9079aca4bde107494864d54f0d72d9db508a51ff0878593/contourpy-1.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:75ee7cb1a14c617f34a51d11fa7524173e56551646828353c4af859c56b766e2", size = 1260894, upload-time = "2024-08-27T20:55:31.553Z" }, + { url = "https://files.pythonhosted.org/packages/2b/1e/1e726ba66eddf21c940821df8cf1a7d15cb165f0682d62161eaa5e93dae1/contourpy-1.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:33c92cdae89ec5135d036e7218e69b0bb2851206077251f04a6c4e0e21f03927", size = 1314829, upload-time = "2024-08-27T20:55:47.837Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e3/b9f72758adb6ef7397327ceb8b9c39c75711affb220e4f53c745ea1d5a9a/contourpy-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a11077e395f67ffc2c44ec2418cfebed032cd6da3022a94fc227b6faf8e2acb8", size = 265518, upload-time = "2024-08-27T20:56:01.333Z" }, + { url = "https://files.pythonhosted.org/packages/ec/22/19f5b948367ab5260fb41d842c7a78dae645603881ea6bc39738bcfcabf6/contourpy-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e8134301d7e204c88ed7ab50028ba06c683000040ede1d617298611f9dc6240c", size = 249350, upload-time = "2024-08-27T20:56:05.432Z" }, + { url = "https://files.pythonhosted.org/packages/26/76/0c7d43263dd00ae21a91a24381b7e813d286a3294d95d179ef3a7b9fb1d7/contourpy-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e12968fdfd5bb45ffdf6192a590bd8ddd3ba9e58360b29683c6bb71a7b41edca", size = 309167, upload-time = "2024-08-27T20:56:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/96/3b/cadff6773e89f2a5a492c1a8068e21d3fccaf1a1c1df7d65e7c8e3ef60ba/contourpy-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd2a0fc506eccaaa7595b7e1418951f213cf8255be2600f1ea1b61e46a60c55f", size = 348279, upload-time = "2024-08-27T20:56:15.41Z" }, + { url = "https://files.pythonhosted.org/packages/e1/86/158cc43aa549d2081a955ab11c6bdccc7a22caacc2af93186d26f5f48746/contourpy-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4cfb5c62ce023dfc410d6059c936dcf96442ba40814aefbfa575425a3a7f19dc", size = 318519, upload-time = "2024-08-27T20:56:21.813Z" }, + { url = "https://files.pythonhosted.org/packages/05/11/57335544a3027e9b96a05948c32e566328e3a2f84b7b99a325b7a06d2b06/contourpy-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68a32389b06b82c2fdd68276148d7b9275b5f5cf13e5417e4252f6d1a34f72a2", size = 321922, upload-time = "2024-08-27T20:56:26.983Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e3/02114f96543f4a1b694333b92a6dcd4f8eebbefcc3a5f3bbb1316634178f/contourpy-1.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:94e848a6b83da10898cbf1311a815f770acc9b6a3f2d646f330d57eb4e87592e", size = 1258017, upload-time = "2024-08-27T20:56:42.246Z" }, + { url = "https://files.pythonhosted.org/packages/f3/3b/bfe4c81c6d5881c1c643dde6620be0b42bf8aab155976dd644595cfab95c/contourpy-1.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d78ab28a03c854a873787a0a42254a0ccb3cb133c672f645c9f9c8f3ae9d0800", size = 1316773, upload-time = "2024-08-27T20:56:58.58Z" }, + { url = "https://files.pythonhosted.org/packages/f1/17/c52d2970784383cafb0bd918b6fb036d98d96bbf0bc1befb5d1e31a07a70/contourpy-1.3.0-cp39-cp39-win32.whl", hash = "sha256:81cb5ed4952aae6014bc9d0421dec7c5835c9c8c31cdf51910b708f548cf58e5", size = 171353, upload-time = "2024-08-27T20:57:02.718Z" }, + { url = "https://files.pythonhosted.org/packages/53/23/db9f69676308e094d3c45f20cc52e12d10d64f027541c995d89c11ad5c75/contourpy-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:14e262f67bd7e6eb6880bc564dcda30b15e351a594657e55b7eec94b6ef72843", size = 211817, upload-time = "2024-08-27T20:57:06.328Z" }, + { url = "https://files.pythonhosted.org/packages/d1/09/60e486dc2b64c94ed33e58dcfb6f808192c03dfc5574c016218b9b7680dc/contourpy-1.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fe41b41505a5a33aeaed2a613dccaeaa74e0e3ead6dd6fd3a118fb471644fd6c", size = 261886, upload-time = "2024-08-27T20:57:10.863Z" }, + { url = "https://files.pythonhosted.org/packages/19/20/b57f9f7174fcd439a7789fb47d764974ab646fa34d1790551de386457a8e/contourpy-1.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca7e17a65f72a5133bdbec9ecf22401c62bcf4821361ef7811faee695799779", size = 311008, upload-time = "2024-08-27T20:57:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/74/fc/5040d42623a1845d4f17a418e590fd7a79ae8cb2bad2b2f83de63c3bdca4/contourpy-1.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1ec4dc6bf570f5b22ed0d7efba0dfa9c5b9e0431aeea7581aa217542d9e809a4", size = 215690, upload-time = "2024-08-27T20:57:19.321Z" }, + { url = "https://files.pythonhosted.org/packages/2b/24/dc3dcd77ac7460ab7e9d2b01a618cb31406902e50e605a8d6091f0a8f7cc/contourpy-1.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:00ccd0dbaad6d804ab259820fa7cb0b8036bda0686ef844d24125d8287178ce0", size = 261894, upload-time = "2024-08-27T20:57:23.873Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/531642a01cfec39d1682e46b5457b07cf805e3c3c584ec27e2a6223f8f6c/contourpy-1.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ca947601224119117f7c19c9cdf6b3ab54c5726ef1d906aa4a69dfb6dd58102", size = 311099, upload-time = "2024-08-27T20:57:28.58Z" }, + { url = "https://files.pythonhosted.org/packages/38/1e/94bda024d629f254143a134eead69e21c836429a2a6ce82209a00ddcb79a/contourpy-1.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6ec93afeb848a0845a18989da3beca3eec2c0f852322efe21af1931147d12cb", size = 215838, upload-time = "2024-08-27T20:57:32.913Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.2" +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'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" }, + { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" }, + { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" }, + { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" }, + { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" }, + { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, + { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, + { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, + { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, + { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, + { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" }, + { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" }, + { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" }, + { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" }, + { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" }, + { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" }, + { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" }, + { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" }, + { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" }, + { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" }, + { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" }, + { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" }, + { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" }, + { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, +] + +[[package]] +name = "coverage" +version = "7.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/d1/7b18a2e0d2994e4e108dadf16580ec192e0a9c65f7456ccb82ced059f9bf/coverage-7.9.0.tar.gz", hash = "sha256:1a93b43de2233a7670a8bf2520fed8ebd5eea6a65b47417500a9d882b0533fa2", size = 813385, upload-time = "2025-06-11T23:23:34.004Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/25/c83935ed228bd0ce277a9a92b505a4f67b0b15ba0344680974a77452c5dd/coverage-7.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3d494fa4256e3cb161ca1df14a91d2d703c27d60452eb0d4a58bb05f52f676e4", size = 211940, upload-time = "2025-06-11T23:21:47.353Z" }, + { url = "https://files.pythonhosted.org/packages/36/42/c58ca1fec2a346ad12356fac955a9b6d848ab37f632a7cb1bc7476efcf90/coverage-7.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b613efceeabf242978d14e1a65626ec3be67c5261918a82a985f56c2a05475ee", size = 212329, upload-time = "2025-06-11T23:21:50.216Z" }, + { url = "https://files.pythonhosted.org/packages/64/0a/6b61e4348cf7b0a70f7995247cde5cc4b5ef0b61d9718109896c77d9ed0e/coverage-7.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:673a4d2cb7ec78e1f2f6f41039f6785f27bca0f6bc0e722b53a58286d12754e1", size = 241447, upload-time = "2025-06-11T23:21:51.757Z" }, + { url = "https://files.pythonhosted.org/packages/a9/1e/5f7060b909352cba70d34be0e34619659c0ddbef426665e036d5d3046b3c/coverage-7.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1edc2244932e9fed92ad14428b9480a97ecd37c970333688bd35048f6472f260", size = 239322, upload-time = "2025-06-11T23:21:53.826Z" }, + { url = "https://files.pythonhosted.org/packages/f5/78/f4ba669c9bf15b537136b663ccb846032cfb73e28b59458ef6899f18fe07/coverage-7.9.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec8b92a7617faa2017bd44c94583830bab8be175722d420501680abc4f5bc794", size = 240467, upload-time = "2025-06-11T23:21:55.415Z" }, + { url = "https://files.pythonhosted.org/packages/79/38/3246ea3ac68dc6f85afac0cb0362d3703647378b9882d55796c71fe83a1a/coverage-7.9.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d8f3ca1f128f11812d3baf0a482e7f36ffb856ac1ae14de3b5d1adcfb7af955d", size = 240376, upload-time = "2025-06-11T23:21:57.108Z" }, + { url = "https://files.pythonhosted.org/packages/c0/58/ef1f20afbaf9affe2941e7b077a8cf08075c6e3fe5e1dfc3160908b6a1de/coverage-7.9.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c30eed34eb8206d9b8c2d0d9fa342fa98e10f34b1e9e1eb05f79ccbf4499c8ff", size = 239046, upload-time = "2025-06-11T23:21:58.709Z" }, + { url = "https://files.pythonhosted.org/packages/09/ba/d510b05b3ca0da8fe746acf8ac815b2d560d6c4d5c4e0f6eafb2ec27dc33/coverage-7.9.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24e6f8e5f125cd8bff33593a484a079305c9f0be911f76c6432f580ade5c1a17", size = 239318, upload-time = "2025-06-11T23:21:59.987Z" }, + { url = "https://files.pythonhosted.org/packages/82/c7/328a412e3bd78c049180df3f4374bb13a332ed8731ff66f49578d5ebf98c/coverage-7.9.0-cp310-cp310-win32.whl", hash = "sha256:a1b0317b4a8ff4d3703cd7aa642b4f963a71255abe4e878659f768238fab6602", size = 214430, upload-time = "2025-06-11T23:22:01.663Z" }, + { url = "https://files.pythonhosted.org/packages/db/a5/0e788cc4796989d77bfb6b1c58819edc2c65522926f0c08cfe42d1529f2b/coverage-7.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:512b1ea57a11dfa23b7f3d8fe8690fcf8cd983a70ae4c2c262cf5c972618fa15", size = 215350, upload-time = "2025-06-11T23:22:02.957Z" }, + { url = "https://files.pythonhosted.org/packages/9d/91/721a7df15263babfe89caf535a08bacbadebdef87338cf37d40f7400161b/coverage-7.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:55b7b9df45174956e0f719a56cf60c0cb4a7f155668881d00de6384e2a3402f4", size = 212055, upload-time = "2025-06-11T23:22:04.389Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d6/1f4c1eae67e698a8535ede02a6958a7587d06869d33a9b134ecc0e17ee07/coverage-7.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:87bceebbc91a58c9264c43638729fcb45910805b9f86444f93654d988305b3a2", size = 212445, upload-time = "2025-06-11T23:22:06.044Z" }, + { url = "https://files.pythonhosted.org/packages/bd/48/c375a6e6a266efa2d5fbf9b04eac88c87430d1a337b4f383ea8beeeedd44/coverage-7.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81da3b6e289bf9fc7dc159ab6d5222f5330ac6e94a6d06f147ba46e53fa6ec82", size = 245010, upload-time = "2025-06-11T23:22:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/7a/43/ec070ad02a1ee10837555a852b6fa256f8c71a953c209488e027673fc5b6/coverage-7.9.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b361684a91224d4362879c1b1802168d2435ff76666f1b7ba52fc300ad832dbc", size = 242725, upload-time = "2025-06-11T23:22:08.64Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ff/8b8efbd058dd59b489d9c5e27ba5766e895c396dd3bd1b78bebef9808c5f/coverage-7.9.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9a384ea4f77ac0a7e36c9a805ed95ef10f423bdb68b4e9487646cdf548a6a05", size = 244527, upload-time = "2025-06-11T23:22:10.416Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e7/3863f458a3af009a4817656f5b56fa90c7e363d73fef338601b275e979c4/coverage-7.9.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:38a5642aa82ea6de0e4331e346f5ba188a9fdb7d727e00199f55031b85135d0a", size = 244174, upload-time = "2025-06-11T23:22:12.046Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f0/2ff1fa06ccd3c3d653e352b10ddeec511b018890b28dbd3c29b6ea3f742e/coverage-7.9.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8c5ff4ca4890c0b57d3e80850534609493280c0f9e6ea2bd314b10cb8cbd76e0", size = 242227, upload-time = "2025-06-11T23:22:13.438Z" }, + { url = "https://files.pythonhosted.org/packages/32/e2/bae13555436f1d0278e70cfe22a0980eab9809e89361e859c96ffa788cb9/coverage-7.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cd052a0c4727ede06393da3c1df1ae6ef6c079e6bdfefb39079877404b3edc22", size = 242815, upload-time = "2025-06-11T23:22:14.723Z" }, + { url = "https://files.pythonhosted.org/packages/20/7c/e1b5b3313c1e3a5e8f8ced567fee67f18c8f18cebee8af0d69052f445a55/coverage-7.9.0-cp311-cp311-win32.whl", hash = "sha256:f73fd1128165e1d665cb7f863a91d00f073044a672c7dfa04ab400af4d1a9226", size = 214469, upload-time = "2025-06-11T23:22:16.187Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c9/0034d3ccbb7b8f80b1ce8a927ea06e2ba265bd0ba4a9a95a83026ac78dfd/coverage-7.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:dd62d62e782d3add529c8e7943f5600efd0d07dadf3819e5f9917edb4acf85d8", size = 215407, upload-time = "2025-06-11T23:22:17.611Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e1/7473bf679a43638c5ccba6228f45f68d33c3b7414ffae757dbb0bb2f1127/coverage-7.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:f75288785cc9a67aff3b04dafd8d0f0be67306018b224d319d23867a161578d6", size = 213778, upload-time = "2025-06-11T23:22:19.217Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6b/7bdef79e79076c7e3303ce2453072528ed13988210fb7a8702bb3d98ea8c/coverage-7.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:969ed1ed0ab0325b50af3204f9024782180e64fb281f5a2952f479ec60a02aba", size = 212252, upload-time = "2025-06-11T23:22:20.662Z" }, + { url = "https://files.pythonhosted.org/packages/08/fe/7e08dd50c3c3cfdbe822ee11e24da9f418983faefb4f5e52fbffae5beeb2/coverage-7.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1abd41781c874e716aaeecb8b27db5f4f2bc568f2ed8d41228aa087d567674f0", size = 212491, upload-time = "2025-06-11T23:22:22.002Z" }, + { url = "https://files.pythonhosted.org/packages/d4/65/9793cf61b3e4c5647e70aabd5b9470958ffd341c42f90730beeb4d21af9c/coverage-7.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0eb6e99487dffd28c88a4fc2ea4286beaf0207a43388775900c93e56cc5a8ae3", size = 246294, upload-time = "2025-06-11T23:22:23.297Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c9/fc61695132da06a34b27a49e853010a80d66a5534a1dfa770cb38aca71c0/coverage-7.9.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c425c85ddb62b32d44f83fb20044fe32edceceee1db1f978c062eec020a73ea5", size = 243311, upload-time = "2025-06-11T23:22:24.966Z" }, + { url = "https://files.pythonhosted.org/packages/62/0e/559a86887580d0de390e018bddfa632ae0762eeeb065bb5557f319071527/coverage-7.9.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0a1f7676bc90ceba67caa66850d689947d586f204ccf6478400c2bf39da5790", size = 245503, upload-time = "2025-06-11T23:22:26.316Z" }, + { url = "https://files.pythonhosted.org/packages/45/09/344d012dc91e60b8c7afee11ffae18338780c703a5b5fb32d8d82987e7cb/coverage-7.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f17055c50768d710d6abc789c9469d0353574780935e1381b83e63edc49ff530", size = 245313, upload-time = "2025-06-11T23:22:27.936Z" }, + { url = "https://files.pythonhosted.org/packages/d2/2d/151b23e82aaea28aa7e3c0390d893bd1aef685866132aad36034f7d462b8/coverage-7.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:298d2917a6bfadbb272e08545ed026af3965e4d2fe71e3f38bf0a816818b226e", size = 243495, upload-time = "2025-06-11T23:22:29.72Z" }, + { url = "https://files.pythonhosted.org/packages/74/5c/0da7fd4ad44259b4b61bd429dc642c6511314a356ffa782b924bd1ea9e5c/coverage-7.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d9be5d26e5f817d478506e4d3c4ff7b92f17d980670b4791bf05baaa37ce2f88", size = 244727, upload-time = "2025-06-11T23:22:31.112Z" }, + { url = "https://files.pythonhosted.org/packages/de/08/6ccf2847c5c0d8fcc153bd8f4341d89ab50c85e01a15cabe4a546d3e943e/coverage-7.9.0-cp312-cp312-win32.whl", hash = "sha256:dc2784edd9ac9fe8692fc5505667deb0b05d895c016aaaf641031ed4a5f93d53", size = 214636, upload-time = "2025-06-11T23:22:33.257Z" }, + { url = "https://files.pythonhosted.org/packages/79/fa/ae2c14d49475215372772f7638c333deaaacda8f3c5717a75377d1992c82/coverage-7.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:18223198464a6d5549db1934cf77a15deb24bb88652c4f5f7cb21cd3ad853704", size = 215448, upload-time = "2025-06-11T23:22:35.125Z" }, + { url = "https://files.pythonhosted.org/packages/62/a9/45309219ba08b89cae84b2cb4ccfed8f941850aa7721c4914282fb3c1081/coverage-7.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:3b00194ff3c84d4b821822ff6c041f245fc55d0d5c7833fc4311d082e97595e8", size = 213817, upload-time = "2025-06-11T23:22:36.557Z" }, + { url = "https://files.pythonhosted.org/packages/0b/59/449eb05f795d0050007b57a4efee79b540fa6fcccad813a191351964a001/coverage-7.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:122c60e92ab66c9c88e17565f67a91b3b3be5617cb50f73cfd34a4c60ed4aab0", size = 212271, upload-time = "2025-06-11T23:22:38.305Z" }, + { url = "https://files.pythonhosted.org/packages/e0/3b/26852a4fb719a6007b0169c1b52116ed14b61267f0bf3ba1e23db516f352/coverage-7.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:813c11b367a6b3cf37212ec36b230f8d086c22b69dbf62877b40939fb2c79e74", size = 212538, upload-time = "2025-06-11T23:22:39.665Z" }, + { url = "https://files.pythonhosted.org/packages/f6/80/99f82896119f36984a5b9189e71c7310fc036613276560b5884b5ee890d7/coverage-7.9.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f05e0f5e87f23d43fefe49e86655c6209dd4f9f034786b983e6803cf4554183", size = 245705, upload-time = "2025-06-11T23:22:41.103Z" }, + { url = "https://files.pythonhosted.org/packages/a9/29/0b007deb096dd527c42e933129a8e4d5f9f1026f4953979c3a1e60e7ea9f/coverage-7.9.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62f465886fa4f86d5515da525aead97c5dff13a5cf997fc4c5097a1a59e063b2", size = 242918, upload-time = "2025-06-11T23:22:42.88Z" }, + { url = "https://files.pythonhosted.org/packages/6f/eb/273855b57c7fb387dd9787f250b8b333ba8c1c100877c21e32eb1b24ff29/coverage-7.9.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:549ea4ca901595bbe3270e1afdef98bf5d4d5791596efbdc90b00449a2bb1f91", size = 244902, upload-time = "2025-06-11T23:22:44.563Z" }, + { url = "https://files.pythonhosted.org/packages/20/57/4e411b47dbfd831538ecf9e5f407e42888b0c56aedbfe0ea7b102a787559/coverage-7.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8cae1d4450945c74a6a65a09864ed3eaa917055cf70aa65f83ac1b9b0d8d5f9a", size = 245069, upload-time = "2025-06-11T23:22:46.352Z" }, + { url = "https://files.pythonhosted.org/packages/91/75/b24cf5703fb325fc4b1899d89984dac117b99e757b9fadd525cad7ecc020/coverage-7.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d7b263910234c0d5ec913ec79ca921152fe874b805a7bcaf67118ef71708e5d2", size = 243040, upload-time = "2025-06-11T23:22:48.147Z" }, + { url = "https://files.pythonhosted.org/packages/c7/e1/9495751d5315c3d76ee2c7b5dbc1935ab891d45ad585e1910a333dbdef43/coverage-7.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7d7b7425215963da8f5968096a20c5b5c9af4a86a950fcc25dcc2177ab33e9e5", size = 244424, upload-time = "2025-06-11T23:22:49.574Z" }, + { url = "https://files.pythonhosted.org/packages/94/2a/ee504188a586da2379939f37fdc69047d9c46d35c34d1196f2605974a17d/coverage-7.9.0-cp313-cp313-win32.whl", hash = "sha256:e7dcfa92867b0c53d2e22e985c66af946dc09e8bb13c556709e396e90a0adf5c", size = 214677, upload-time = "2025-06-11T23:22:51.394Z" }, + { url = "https://files.pythonhosted.org/packages/80/2b/5eab6518643c7560fe180ba5e0f35a0be3d4fc0a88aa6601120407b1fd03/coverage-7.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:aa34ca040785a2b768da489df0c036364d47a6c1c00bdd8f662b98fd3277d3d4", size = 215482, upload-time = "2025-06-11T23:22:53.151Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7f/9c9c8b736c4f40d7247bea8339afac40d8f6465491440608b3d73c10ffce/coverage-7.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:9c5dcb5cd3c52d84c5f52045e1c87c16bf189c2fbfa57cc0d811a3b4059939df", size = 213852, upload-time = "2025-06-11T23:22:54.568Z" }, + { url = "https://files.pythonhosted.org/packages/e5/83/056464aec8b360dee6f4d7a517dc5ae5a9f462ff895ff536588b42f95b2d/coverage-7.9.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b52d2fdc1940f90c4572bd48211475a7b102f75a7f9a5e6cfc6e3da7dc380c44", size = 212994, upload-time = "2025-06-11T23:22:56.173Z" }, + { url = "https://files.pythonhosted.org/packages/a3/87/f0291ecaa6baaaedbd428cf8b7e1d16b5dc010718fe7739cce955149ef83/coverage-7.9.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4cc555a3e6ceb8841df01a4634374f5f9635e661f5c307da00bce19819e8bcdf", size = 213212, upload-time = "2025-06-11T23:22:58.051Z" }, + { url = "https://files.pythonhosted.org/packages/16/a0/9eb39541774a5beb662dc4ae98fee23afb947414b6aa1443b53d2ad3ea05/coverage-7.9.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:244f613617876b7cd32a097788d49c952a8f1698afb25275b2a825a4e895854e", size = 256453, upload-time = "2025-06-11T23:22:59.485Z" }, + { url = "https://files.pythonhosted.org/packages/93/33/d0e99f4c809334dfed20f17234080a9003a713ddb80e33ad22697a8aa8e5/coverage-7.9.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5c335d77539e66bc6f83e8f1ef207d038129d9b9acd9dc9f0ca42fa9eedf564a", size = 252674, upload-time = "2025-06-11T23:23:00.984Z" }, + { url = "https://files.pythonhosted.org/packages/0b/3a/d2a64e7ee5eb783e44e6ca404f8fc2a45afef052ed6593afb4ce9663dae6/coverage-7.9.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b335c7077c8da7bb8173d4f9ebd90ff1a97af6a6bec4fc4e6db4856ae80b31e", size = 254830, upload-time = "2025-06-11T23:23:02.445Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6a/9de640f8e2b097d155532d1bc16eb9c5186fccc7c4b8148fe1dd2520875a/coverage-7.9.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:01cbc2c36895b7ab906514042c92b3fc9dd0526bf1c3251cb6aefd9c71ae6dda", size = 256060, upload-time = "2025-06-11T23:23:03.89Z" }, + { url = "https://files.pythonhosted.org/packages/07/72/928fa3583b9783fc32e3dfafb6cc0cf73bdd73d1dc41e3a973f203c6aeff/coverage-7.9.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1ac62880a9dff0726a193ce77a1bcdd4e8491009cb3a0510d31381e8b2c46d7a", size = 254174, upload-time = "2025-06-11T23:23:05.366Z" }, + { url = "https://files.pythonhosted.org/packages/ad/01/2fd0785f8768693b748e36b442352bc26edf3391246eedcc80d480d06da1/coverage-7.9.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:95314eb306cf54af3d1147e27ba008cf78eed6f1309a1310772f4f05b12c9c65", size = 255011, upload-time = "2025-06-11T23:23:07.212Z" }, + { url = "https://files.pythonhosted.org/packages/b7/49/1d0120cfa24e001e0d38795388914183c48cd86fc8640ca3b01337831917/coverage-7.9.0-cp313-cp313t-win32.whl", hash = "sha256:c5cbf3ddfb68de8dc8ce33caa9321df27297a032aeaf2e99b278f183fb4ebc37", size = 215349, upload-time = "2025-06-11T23:23:09.037Z" }, + { url = "https://files.pythonhosted.org/packages/9f/48/7625c09621a206fff0b51fcbcf5d6c1162ab10a5ffa546fc132f01c9132b/coverage-7.9.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e3ec9e1525eb7a0f89d31083539b398d921415d884e9f55400002a1e9fe0cf63", size = 216516, upload-time = "2025-06-11T23:23:11.083Z" }, + { url = "https://files.pythonhosted.org/packages/bb/50/048b55c34985c3aafcecb32cced3abc4291969bfd967dbcaed95cfc26b2a/coverage-7.9.0-cp313-cp313t-win_arm64.whl", hash = "sha256:a02efe6769f74245ce476e89db3d4e110db07b4c0c3d3f81728e2464bbbbcb8e", size = 214308, upload-time = "2025-06-11T23:23:12.522Z" }, + { url = "https://files.pythonhosted.org/packages/c3/4e/4c72909d117d593e388c82b8bc29f99ad0fe20fe84f6390ee14d5650b750/coverage-7.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:64dab59d812c1cbfc9cebadada377365874964acdf59b12e86487d25c2e0c29f", size = 211938, upload-time = "2025-06-11T23:23:14.301Z" }, + { url = "https://files.pythonhosted.org/packages/84/84/8e2e1ebe02a5c68c4ac54668392ee00fa5ea8e7989b339d847fff27220bd/coverage-7.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46b9dc640c6309fb49625d3569d4ba7abe2afcba645eb1e52bad97510f60ac26", size = 212314, upload-time = "2025-06-11T23:23:15.816Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/931117485d6917f4719be2bf8cc25c79c7108c078b005b38882688e1f41b/coverage-7.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89358f4025ed424861311b33815a2866f7c94856c932b0ffc98180f655e813e2", size = 241077, upload-time = "2025-06-11T23:23:17.382Z" }, + { url = "https://files.pythonhosted.org/packages/f9/58/431fbfb00a4dfc1d845b70d296b503d306be76d07a67a4046b15e42c8234/coverage-7.9.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:589e37ae75d81fd53cd1ca624e07af4466e9e4ce259e3bfe2b147896857c06ea", size = 238945, upload-time = "2025-06-11T23:23:18.936Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e2/8b2cc9b761bee876472379db92d017d7042eeaddba35adf67f54e3ceff3d/coverage-7.9.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29dea81eef5432076cee561329b3831bc988a4ce1bfaec90eee2078ff5311e6e", size = 240063, upload-time = "2025-06-11T23:23:20.883Z" }, + { url = "https://files.pythonhosted.org/packages/6c/39/e1b0ba8cac5ae66a13475cb08b184f06d89515b6ea6ed45cd678ae2fbcb1/coverage-7.9.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7b3482588772b6b24601d1677aef299af28d6c212c70b0be27bdfc2e10fb00fe", size = 239789, upload-time = "2025-06-11T23:23:22.739Z" }, + { url = "https://files.pythonhosted.org/packages/6e/91/b6b926cd875cd03989abb696ccbbd5895e367e6394dcf7c264180f72d038/coverage-7.9.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2debc0b9481b5fc76f771b3b31e89a0cd8791ad977654940a3523f3f2e5d98fe", size = 238041, upload-time = "2025-06-11T23:23:24.531Z" }, + { url = "https://files.pythonhosted.org/packages/43/ce/de736582c44906b5d6067b650ac851d5f249e246753b9d8f7369e7eea00a/coverage-7.9.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:304ded640bc2a60f14a2ff0fec98cce4c3f2e573c122f0548728c8dceba5abe7", size = 238977, upload-time = "2025-06-11T23:23:26.168Z" }, + { url = "https://files.pythonhosted.org/packages/27/d3/35317997155b16b140a2c62f09e001a12e244b2d410deb5b8cfa861173f4/coverage-7.9.0-cp39-cp39-win32.whl", hash = "sha256:8e0a3a3f9b968007e1f56418a3586f9a983c84ac4e84d28d1c4f8b76c4226282", size = 214442, upload-time = "2025-06-11T23:23:27.774Z" }, + { url = "https://files.pythonhosted.org/packages/0a/42/d4bcd2900c05bdb5773d47173395c68c147b4ca2564e791c8c9b0ed42c73/coverage-7.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:cb3c07dd71d1ff52156d35ee6fa48458c3cec1add7fcce6a934f977fb80c48a5", size = 215351, upload-time = "2025-06-11T23:23:29.383Z" }, + { url = "https://files.pythonhosted.org/packages/e8/b6/d16966f9439ccc3007e1740960d241420d6ba81502642a4be1da1672a103/coverage-7.9.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:ccf1540a0e82ff525844880f988f6caaa2d037005e57bfe203b71cac7626145d", size = 203927, upload-time = "2025-06-11T23:23:30.913Z" }, + { url = "https://files.pythonhosted.org/packages/70/0d/534c1e35cb7688b5c40de93fcca07e3ddc0287659ff85cd376b1dd3f770f/coverage-7.9.0-py3-none-any.whl", hash = "sha256:79ea9a26b27c963cdf541e1eb9ac05311b012bc367d0e31816f1833b06c81c02", size = 203917, upload-time = "2025-06-11T23:23:32.413Z" }, +] + +[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 = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "cython" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/40/7b17cd866158238db704965da1b5849af261dbad393ea3ac966f934b2d39/cython-3.1.2.tar.gz", hash = "sha256:6bbf7a953fa6762dfecdec015e3b054ba51c0121a45ad851fa130f63f5331381", size = 3184825, upload-time = "2025-06-09T07:08:48.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/5e/c89172b252697acd6a440a2efead37685f8f2c42ea0d906098cbfb9aed69/cython-3.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0f2add8b23cb19da3f546a688cd8f9e0bfc2776715ebf5e283bc3113b03ff008", size = 2973977, upload-time = "2025-06-09T07:09:03.604Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e5/d7fb67187193c5763d59a4b70d86a92be18b05b01737af8bfca7bafea0d3/cython-3.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0d6248a2ae155ca4c42d7fa6a9a05154d62e695d7736bc17e1b85da6dcc361df", size = 2836988, upload-time = "2025-06-09T07:09:06.156Z" }, + { url = "https://files.pythonhosted.org/packages/23/3a/5b92bfff9c1cc1179a493684d0e6a893ee7cd69c4f1977813000ea76c5d7/cython-3.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:262bf49d9da64e2a34c86cbf8de4aa37daffb0f602396f116cca1ed47dc4b9f2", size = 3212933, upload-time = "2025-06-09T07:09:08.725Z" }, + { url = "https://files.pythonhosted.org/packages/b4/eb/8c47ba21177929f9122e7aceca9fe1f9f5a037e705226f8a5a9113fb53ba/cython-3.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae53ae93c699d5f113953a9869df2fc269d8e173f9aa0616c6d8d6e12b4e9827", size = 3332955, upload-time = "2025-06-09T07:09:11.371Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a7/e29079146154c4c0403dfb5b9b51c183e0887fc19727aacc3946246c5898/cython-3.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b417c5d046ce676ee595ec7955ed47a68ad6f419cbf8c2a8708e55a3b38dfa35", size = 3394613, upload-time = "2025-06-09T07:09:14.189Z" }, + { url = "https://files.pythonhosted.org/packages/94/18/dd10c4531c0e918b20300ee23b32a4bffa5cbacaa8e8dd19fa6b02b260fe/cython-3.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:af127da4b956e0e906e552fad838dc3fb6b6384164070ceebb0d90982a8ae25a", size = 3257573, upload-time = "2025-06-09T07:09:16.787Z" }, + { url = "https://files.pythonhosted.org/packages/19/09/0998fa0c42c6cc56fdcba6bb757abe13fc4456a5a063dacb5331e30d7560/cython-3.1.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9be3d4954b46fd0f2dceac011d470f658eaf819132db52fbd1cf226ee60348db", size = 3479007, upload-time = "2025-06-09T07:09:19.431Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1c/e107d8bc45ab1f3c2205c7f4a17b3c594126b72f7fc2d78b304f5ae72434/cython-3.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:63da49672c4bb022b4de9d37bab6c29953dbf5a31a2f40dffd0cf0915dcd7a17", size = 3414055, upload-time = "2025-06-09T07:09:22.264Z" }, + { url = "https://files.pythonhosted.org/packages/13/25/5c1177bbc23263ba82b60a754383a001c57798d3f7982ea9b5fd3916c1fa/cython-3.1.2-cp310-cp310-win32.whl", hash = "sha256:2d8291dbbc1cb86b8d60c86fe9cbf99ec72de28cb157cbe869c95df4d32efa96", size = 2484860, upload-time = "2025-06-09T07:09:24.203Z" }, + { url = "https://files.pythonhosted.org/packages/f5/19/119287fa7e3c8268d33ac6213fc7e7d6e9b74b239d459073d285362ebf2a/cython-3.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:e1f30a1339e03c80968a371ef76bf27a6648c5646cccd14a97e731b6957db97a", size = 2679771, upload-time = "2025-06-09T07:09:26.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/de/502ddebaf5fe78f13cd6361acdd74710d3a5b15c22a9edc0ea4c873a59a5/cython-3.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5548573e0912d7dc80579827493315384c462e2f15797b91a8ed177686d31eb9", size = 3007792, upload-time = "2025-06-09T07:09:28.777Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c8/91b00bc68effba9ba1ff5b33988052ac4d98fc1ac3021ade7261661299c6/cython-3.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bf3ea5bc50d80762c490f42846820a868a6406fdb5878ae9e4cc2f11b50228a", size = 2870798, upload-time = "2025-06-09T07:09:30.745Z" }, + { url = "https://files.pythonhosted.org/packages/f4/4b/29d290f14607785112c00a5e1685d766f433531bbd6a11ad229ab61b7a70/cython-3.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20ce53951d06ab2bca39f153d9c5add1d631c2a44d58bf67288c9d631be9724e", size = 3131280, upload-time = "2025-06-09T07:09:32.785Z" }, + { url = "https://files.pythonhosted.org/packages/38/3c/7c61e9ce25377ec7c4aa0b7ceeed34559ebca7b5cfd384672ba64eeaa4ba/cython-3.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e05a36224e3002d48c7c1c695b3771343bd16bc57eab60d6c5d5e08f3cbbafd8", size = 3223898, upload-time = "2025-06-09T07:09:35.345Z" }, + { url = "https://files.pythonhosted.org/packages/10/96/2d3fbe7e50e98b53ac86fefb48b64262b2e1304b3495e8e25b3cd1c3473e/cython-3.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbc0fc0777c7ab82297c01c61a1161093a22a41714f62e8c35188a309bd5db8e", size = 3291527, upload-time = "2025-06-09T07:09:37.502Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e4/4cd3624e250d86f05bdb121a567865b9cca75cdc6dce4eedd68e626ea4f8/cython-3.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:18161ef3dd0e90a944daa2be468dd27696712a5f792d6289e97d2a31298ad688", size = 3184034, upload-time = "2025-06-09T07:09:40.225Z" }, + { url = "https://files.pythonhosted.org/packages/24/de/f8c1243c3e50ec95cb81f3a7936c8cf162f28050db8683e291c3861b46a0/cython-3.1.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ca45020950cd52d82189d6dfb6225737586be6fe7b0b9d3fadd7daca62eff531", size = 3386084, upload-time = "2025-06-09T07:09:42.206Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/2365937da44741ef0781bb9ecc1f8f52b38b65acb7293b5fc7c3eaee5346/cython-3.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:aaae97d6d07610224be2b73a93e9e3dd85c09aedfd8e47054e3ef5a863387dae", size = 3309974, upload-time = "2025-06-09T07:09:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b8/280eed114110a1a3aa9e2e76bcd06cdd5ef0df7ab77c0be9d5378ca28c57/cython-3.1.2-cp311-cp311-win32.whl", hash = "sha256:3d439d9b19e7e70f6ff745602906d282a853dd5219d8e7abbf355de680c9d120", size = 2482942, upload-time = "2025-06-09T07:09:46.583Z" }, + { url = "https://files.pythonhosted.org/packages/a2/50/0aa65be5a4ab65bde3224b8fd23ed795f699d1e724ac109bb0a32036b82d/cython-3.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:8efa44ee2f1876e40eb5e45f6513a19758077c56bf140623ccab43d31f873b61", size = 2686535, upload-time = "2025-06-09T07:09:48.345Z" }, + { url = "https://files.pythonhosted.org/packages/22/86/9393ab7204d5bb65f415dd271b658c18f57b9345d06002cae069376a5a7a/cython-3.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9c2c4b6f9a941c857b40168b3f3c81d514e509d985c2dcd12e1a4fea9734192e", size = 3015898, upload-time = "2025-06-09T07:09:50.79Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b8/3d10ac37ab7b7ee60bc6bfb48f6682ebee7fddaccf56e1e135f0d46ca79f/cython-3.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bdbc115bbe1b8c1dcbcd1b03748ea87fa967eb8dfc3a1a9bb243d4a382efcff4", size = 2846204, upload-time = "2025-06-09T07:09:52.832Z" }, + { url = "https://files.pythonhosted.org/packages/f8/34/637771d8e10ebabc34a34cdd0d63fe797b66c334e150189955bf6442d710/cython-3.1.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05111f89db1ca98edc0675cfaa62be47b3ff519a29876eb095532a9f9e052b8", size = 3080671, upload-time = "2025-06-09T07:09:54.924Z" }, + { url = "https://files.pythonhosted.org/packages/6b/c8/383ad1851fb272920a152c5a30bb6f08c3471b5438079d9488fc3074a170/cython-3.1.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e7188df8709be32cfdfadc7c3782e361c929df9132f95e1bbc90a340dca3c7", size = 3199022, upload-time = "2025-06-09T07:09:56.978Z" }, + { url = "https://files.pythonhosted.org/packages/e6/11/20adc8f2db37a29f245e8fd4b8b8a8245fce4bbbd128185cc9a7b1065e4c/cython-3.1.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c0ecc71e60a051732c2607b8eb8f2a03a5dac09b28e52b8af323c329db9987b", size = 3241337, upload-time = "2025-06-09T07:09:59.156Z" }, + { url = "https://files.pythonhosted.org/packages/6f/0b/491f1fd3e177cccb6bb6d52f9609f78d395edde83ac47ebb06d21717ca29/cython-3.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f27143cf88835c8bcc9bf3304953f23f377d1d991e8942982fe7be344c7cfce3", size = 3131808, upload-time = "2025-06-09T07:10:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/db/d2/5e7053a3214c9baa7ad72940555eb87cf4750e597f10b2bb43db62c3f39f/cython-3.1.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d8c43566701133f53bf13485839d8f3f309095fe0d3b9d0cd5873073394d2edc", size = 3340319, upload-time = "2025-06-09T07:10:03.485Z" }, + { url = "https://files.pythonhosted.org/packages/95/42/4842f8ddac9b36c94ae08b23c7fcde3f930c1dd49ac8992bb5320a4d96b5/cython-3.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a3bb893e85f027a929c1764bb14db4c31cbdf8a96f59a78f608f2ba7cfbbce95", size = 3287370, upload-time = "2025-06-09T07:10:05.637Z" }, + { url = "https://files.pythonhosted.org/packages/03/0d/417745ed75d414176e50310087b43299a3e611e75c379ff998f60f2ca1a8/cython-3.1.2-cp312-cp312-win32.whl", hash = "sha256:12c5902f105e43ca9af7874cdf87a23627f98c15d5a4f6d38bc9d334845145c0", size = 2487734, upload-time = "2025-06-09T07:10:07.591Z" }, + { url = "https://files.pythonhosted.org/packages/8e/82/df61d09ab81979ba171a8252af8fb8a3b26a0f19d1330c2679c11fe41667/cython-3.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:06789eb7bd2e55b38b9dd349e9309f794aee0fed99c26ea5c9562d463877763f", size = 2695542, upload-time = "2025-06-09T07:10:09.545Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e2/355354a00a4ee7029b89767a280272f91c7e68b6edb686690992aaa6c32c/cython-3.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cc22e5f18af436c894b90c257130346930fdc860d7f42b924548c591672beeef", size = 2999991, upload-time = "2025-06-09T07:10:11.825Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d6/fb1033396585fd900adda9a410624b96d2a37b5f7f3685f0bdc5fa2bafe0/cython-3.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42c7bffb0fe9898996c7eef9eb74ce3654553c7a3a3f3da66e5a49f801904ce0", size = 2831764, upload-time = "2025-06-09T07:10:14.578Z" }, + { url = "https://files.pythonhosted.org/packages/28/46/2bbcd5a8a67e4ec0dbdf73b0b85add085e401d782cdc9291673aeaf05fc2/cython-3.1.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88dc7fd54bfae78c366c6106a759f389000ea4dfe8ed9568af9d2f612825a164", size = 3068467, upload-time = "2025-06-09T07:10:17.158Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9b/20a8a12d1454416141479380f7722f2ad298d2b41d0d7833fc409894715d/cython-3.1.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80d0ce057672ca50728153757d022842d5dcec536b50c79615a22dda2a874ea0", size = 3186690, upload-time = "2025-06-09T07:10:19.257Z" }, + { url = "https://files.pythonhosted.org/packages/3a/9f/20cdecae7966dfbaff198952bcee745e402072a3b6565dfebb41202b55f8/cython-3.1.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eda6a43f1b78eae0d841698916eef661d15f8bc8439c266a964ea4c504f05612", size = 3212888, upload-time = "2025-06-09T07:10:22.648Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6a/ae723af7a2c9fe9e737468c046d953b34d427093c4974d34c15174cf7efe/cython-3.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b4c516d103e87c2e9c1ab85227e4d91c7484c1ba29e25f8afbf67bae93fee164", size = 3117859, upload-time = "2025-06-09T07:10:24.772Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e9/25a5f5c962f2f331dc2ff74a62046e35ec0ffd08f0da6fa51261101a5e2e/cython-3.1.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7542f1d18ab2cd22debc72974ec9e53437a20623d47d6001466e430538d7df54", size = 3315382, upload-time = "2025-06-09T07:10:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/4f/d2/2ee59f5e31b1d7e397ca0f3899559681a44dd3502fa8b68d2bb285f54aa7/cython-3.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:63335513c06dcec4ecdaa8598f36c969032149ffd92a461f641ee363dc83c7ad", size = 3273216, upload-time = "2025-06-09T07:10:29.603Z" }, + { url = "https://files.pythonhosted.org/packages/a7/88/e792eb40d8a17010793da2f6c0f72624ec2b7964fccba8d5c544aed16400/cython-3.1.2-cp313-cp313-win32.whl", hash = "sha256:b377d542299332bfeb61ec09c57821b10f1597304394ba76544f4d07780a16df", size = 2482057, upload-time = "2025-06-09T07:10:31.547Z" }, + { url = "https://files.pythonhosted.org/packages/c2/94/65ba40faeafe74845ba22b61aff7d73475671c3bd24bffc6cba53f3b0063/cython-3.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:8ab1319c77f15b0ae04b3fb03588df3afdec4cf79e90eeea5c961e0ebd8fdf72", size = 2693103, upload-time = "2025-06-09T07:10:33.696Z" }, + { url = "https://files.pythonhosted.org/packages/7b/27/e402bf7f793773d7c2048d10e6727c97a23fd948d32c34a1e8b9ff6eea3f/cython-3.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe7f1ee4c13f8a773bd6c66b3d25879f40596faeab49f97d28c39b16ace5fff9", size = 2982846, upload-time = "2025-06-09T07:10:59.318Z" }, + { url = "https://files.pythonhosted.org/packages/55/ba/da6cc131bb9323d804971056c283bc1bb977d7e3ba032dfe8866a3f47f6c/cython-3.1.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c9ec7d2baea122d94790624f743ff5b78f4e777bf969384be65b69d92fa4bc3f", size = 2845036, upload-time = "2025-06-09T07:11:01.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/26/52334194ab805c717e85b9bf45d156d9db44cd67841c637c1132ae1e4b0d/cython-3.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df57827185874f29240b02402e615547ab995d90182a852c6ec4f91bbae355a4", size = 3220181, upload-time = "2025-06-09T07:11:03.573Z" }, + { url = "https://files.pythonhosted.org/packages/18/90/7dc13bd8621b25caa57d6047938ac5e324c763828fccfe956c2a38b22f90/cython-3.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1a69b9b4fe0a48a8271027c0703c71ab1993c4caca01791c0fd2e2bd9031aa", size = 3339784, upload-time = "2025-06-09T07:11:05.73Z" }, + { url = "https://files.pythonhosted.org/packages/13/b2/9c4d8eb69cd4d772a889cea3b2840a726d703d04cc1786e27be6956184a7/cython-3.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:970cc1558519f0f108c3e2f4b3480de4945228d9292612d5b2bb687e36c646b8", size = 3402532, upload-time = "2025-06-09T07:11:07.952Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ea/9235a08c2e645170c94502e13abbe9684b050c87efd0ba3320d1a8d3a3b1/cython-3.1.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:604c39cd6d152498a940aeae28b6fd44481a255a3fdf1b0051c30f3873c88b7f", size = 3263419, upload-time = "2025-06-09T07:11:10.528Z" }, + { url = "https://files.pythonhosted.org/packages/59/d0/5a6c717d4738392cb3a12f044023b9cc841379d031d99e26d12cf4cca39f/cython-3.1.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:855f2ae06438c7405997cf0df42d5b508ec3248272bb39df4a7a4a82a5f7c8cb", size = 3487142, upload-time = "2025-06-09T07:11:12.887Z" }, + { url = "https://files.pythonhosted.org/packages/42/06/e161f72adff8f8cffb5a66c4937510a83d552dff5a6017b72c081f9d85ec/cython-3.1.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9e3016ca7a86728bfcbdd52449521e859a977451f296a7ae4967cefa2ec498f7", size = 3417671, upload-time = "2025-06-09T07:11:15.34Z" }, + { url = "https://files.pythonhosted.org/packages/07/24/501c130ecd6a5044eb674bec05f2527fe46068e2072a19395f0f110b2962/cython-3.1.2-cp39-cp39-win32.whl", hash = "sha256:4896fc2b0f90820ea6fcf79a07e30822f84630a404d4e075784124262f6d0adf", size = 2489689, upload-time = "2025-06-09T07:11:17.364Z" }, + { url = "https://files.pythonhosted.org/packages/24/15/3a2d377435ff30b4fc5b5bd97104523cdb64f831f90140b005b9eee47fed/cython-3.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:a965b81eb4f5a5f3f6760b162cb4de3907c71a9ba25d74de1ad7a0e4856f0412", size = 2685214, upload-time = "2025-06-09T07:11:19.43Z" }, + { url = "https://files.pythonhosted.org/packages/25/d6/ef8557d5e75cc57d55df579af4976935ee111a85bbee4a5b72354e257066/cython-3.1.2-py3-none-any.whl", hash = "sha256:d23fd7ffd7457205f08571a42b108a3cf993e83a59fe4d72b42e6fc592cf2639", size = 1224753, upload-time = "2025-06-09T07:08:44.849Z" }, +] + +[[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 = "debugpy" +version = "1.8.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/75/087fe07d40f490a78782ff3b0a30e3968936854105487decdb33446d4b0e/debugpy-1.8.14.tar.gz", hash = "sha256:7cd287184318416850aa8b60ac90105837bb1e59531898c07569d197d2ed5322", size = 1641444, upload-time = "2025-04-10T19:46:10.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/df/156df75a41aaebd97cee9d3870fe68f8001b6c1c4ca023e221cfce69bece/debugpy-1.8.14-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:93fee753097e85623cab1c0e6a68c76308cd9f13ffdf44127e6fab4fbf024339", size = 2076510, upload-time = "2025-04-10T19:46:13.315Z" }, + { url = "https://files.pythonhosted.org/packages/69/cd/4fc391607bca0996db5f3658762106e3d2427beaef9bfd363fd370a3c054/debugpy-1.8.14-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d937d93ae4fa51cdc94d3e865f535f185d5f9748efb41d0d49e33bf3365bd79", size = 3559614, upload-time = "2025-04-10T19:46:14.647Z" }, + { url = "https://files.pythonhosted.org/packages/1a/42/4e6d2b9d63e002db79edfd0cb5656f1c403958915e0e73ab3e9220012eec/debugpy-1.8.14-cp310-cp310-win32.whl", hash = "sha256:c442f20577b38cc7a9aafecffe1094f78f07fb8423c3dddb384e6b8f49fd2987", size = 5208588, upload-time = "2025-04-10T19:46:16.233Z" }, + { url = "https://files.pythonhosted.org/packages/97/b1/cc9e4e5faadc9d00df1a64a3c2d5c5f4b9df28196c39ada06361c5141f89/debugpy-1.8.14-cp310-cp310-win_amd64.whl", hash = "sha256:f117dedda6d969c5c9483e23f573b38f4e39412845c7bc487b6f2648df30fe84", size = 5241043, upload-time = "2025-04-10T19:46:17.768Z" }, + { url = "https://files.pythonhosted.org/packages/67/e8/57fe0c86915671fd6a3d2d8746e40485fd55e8d9e682388fbb3a3d42b86f/debugpy-1.8.14-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:1b2ac8c13b2645e0b1eaf30e816404990fbdb168e193322be8f545e8c01644a9", size = 2175064, upload-time = "2025-04-10T19:46:19.486Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/2b2fd1b1c9569c6764ccdb650a6f752e4ac31be465049563c9eb127a8487/debugpy-1.8.14-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf431c343a99384ac7eab2f763980724834f933a271e90496944195318c619e2", size = 3132359, upload-time = "2025-04-10T19:46:21.192Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ee/b825c87ed06256ee2a7ed8bab8fb3bb5851293bf9465409fdffc6261c426/debugpy-1.8.14-cp311-cp311-win32.whl", hash = "sha256:c99295c76161ad8d507b413cd33422d7c542889fbb73035889420ac1fad354f2", size = 5133269, upload-time = "2025-04-10T19:46:23.047Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a6/6c70cd15afa43d37839d60f324213843174c1d1e6bb616bd89f7c1341bac/debugpy-1.8.14-cp311-cp311-win_amd64.whl", hash = "sha256:7816acea4a46d7e4e50ad8d09d963a680ecc814ae31cdef3622eb05ccacf7b01", size = 5158156, upload-time = "2025-04-10T19:46:24.521Z" }, + { url = "https://files.pythonhosted.org/packages/d9/2a/ac2df0eda4898f29c46eb6713a5148e6f8b2b389c8ec9e425a4a1d67bf07/debugpy-1.8.14-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:8899c17920d089cfa23e6005ad9f22582fd86f144b23acb9feeda59e84405b84", size = 2501268, upload-time = "2025-04-10T19:46:26.044Z" }, + { url = "https://files.pythonhosted.org/packages/10/53/0a0cb5d79dd9f7039169f8bf94a144ad3efa52cc519940b3b7dde23bcb89/debugpy-1.8.14-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6bb5c0dcf80ad5dbc7b7d6eac484e2af34bdacdf81df09b6a3e62792b722826", size = 4221077, upload-time = "2025-04-10T19:46:27.464Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d5/84e01821f362327bf4828728aa31e907a2eca7c78cd7c6ec062780d249f8/debugpy-1.8.14-cp312-cp312-win32.whl", hash = "sha256:281d44d248a0e1791ad0eafdbbd2912ff0de9eec48022a5bfbc332957487ed3f", size = 5255127, upload-time = "2025-04-10T19:46:29.467Z" }, + { url = "https://files.pythonhosted.org/packages/33/16/1ed929d812c758295cac7f9cf3dab5c73439c83d9091f2d91871e648093e/debugpy-1.8.14-cp312-cp312-win_amd64.whl", hash = "sha256:5aa56ef8538893e4502a7d79047fe39b1dae08d9ae257074c6464a7b290b806f", size = 5297249, upload-time = "2025-04-10T19:46:31.538Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e4/395c792b243f2367d84202dc33689aa3d910fb9826a7491ba20fc9e261f5/debugpy-1.8.14-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:329a15d0660ee09fec6786acdb6e0443d595f64f5d096fc3e3ccf09a4259033f", size = 2485676, upload-time = "2025-04-10T19:46:32.96Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f1/6f2ee3f991327ad9e4c2f8b82611a467052a0fb0e247390192580e89f7ff/debugpy-1.8.14-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f920c7f9af409d90f5fd26e313e119d908b0dd2952c2393cd3247a462331f15", size = 4217514, upload-time = "2025-04-10T19:46:34.336Z" }, + { url = "https://files.pythonhosted.org/packages/79/28/b9d146f8f2dc535c236ee09ad3e5ac899adb39d7a19b49f03ac95d216beb/debugpy-1.8.14-cp313-cp313-win32.whl", hash = "sha256:3784ec6e8600c66cbdd4ca2726c72d8ca781e94bce2f396cc606d458146f8f4e", size = 5254756, upload-time = "2025-04-10T19:46:36.199Z" }, + { url = "https://files.pythonhosted.org/packages/e0/62/a7b4a57013eac4ccaef6977966e6bec5c63906dd25a86e35f155952e29a1/debugpy-1.8.14-cp313-cp313-win_amd64.whl", hash = "sha256:684eaf43c95a3ec39a96f1f5195a7ff3d4144e4a18d69bb66beeb1a6de605d6e", size = 5297119, upload-time = "2025-04-10T19:46:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/6f/96ba96545f55b6a675afa08c96b42810de9b18c7ad17446bbec82762127a/debugpy-1.8.14-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:413512d35ff52c2fb0fd2d65e69f373ffd24f0ecb1fac514c04a668599c5ce7f", size = 2077696, upload-time = "2025-04-10T19:46:46.817Z" }, + { url = "https://files.pythonhosted.org/packages/fa/84/f378a2dd837d94de3c85bca14f1db79f8fcad7e20b108b40d59da56a6d22/debugpy-1.8.14-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c9156f7524a0d70b7a7e22b2e311d8ba76a15496fb00730e46dcdeedb9e1eea", size = 3554846, upload-time = "2025-04-10T19:46:48.72Z" }, + { url = "https://files.pythonhosted.org/packages/db/52/88824fe5d6893f59933f664c6e12783749ab537a2101baf5c713164d8aa2/debugpy-1.8.14-cp39-cp39-win32.whl", hash = "sha256:b44985f97cc3dd9d52c42eb59ee9d7ee0c4e7ecd62bca704891f997de4cef23d", size = 5209350, upload-time = "2025-04-10T19:46:50.284Z" }, + { url = "https://files.pythonhosted.org/packages/41/35/72e9399be24a04cb72cfe1284572c9fcd1d742c7fa23786925c18fa54ad8/debugpy-1.8.14-cp39-cp39-win_amd64.whl", hash = "sha256:b1528cfee6c1b1c698eb10b6b096c598738a8238822d218173d21c3086de8123", size = 5241852, upload-time = "2025-04-10T19:46:52.022Z" }, + { url = "https://files.pythonhosted.org/packages/97/1a/481f33c37ee3ac8040d3d51fc4c4e4e7e61cb08b8bc8971d6032acc2279f/debugpy-1.8.14-py2.py3-none-any.whl", hash = "sha256:5cd9a579d553b6cb9759a7908a41988ee6280b961f24f63336835d9418216a20", size = 5256230, upload-time = "2025-04-10T19:46:54.077Z" }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, +] + +[[package]] +name = "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 = "executing" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/50/a9d80c47ff289c611ff12e63f7c5d13942c65d68125160cefd768c73e6e4/executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755", size = 978693, upload-time = "2025-01-22T15:41:29.403Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702, upload-time = "2025-01-22T15:41:25.929Z" }, +] + +[[package]] +name = "fonttools" +version = "4.58.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/a9/3319c6ae07fd9dde51064ddc6d82a2b707efad8ed407d700a01091121bbc/fonttools-4.58.2.tar.gz", hash = "sha256:4b491ddbfd50b856e84b0648b5f7941af918f6d32f938f18e62b58426a8d50e2", size = 3524285, upload-time = "2025-06-06T14:50:58.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/6f/1f0158cd9d6168258362369fa003c58fc36f2b141a66bc805c76f28f57cc/fonttools-4.58.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4baaf34f07013ba9c2c3d7a95d0c391fcbb30748cb86c36c094fab8f168e49bb", size = 2735491, upload-time = "2025-06-06T14:49:33.45Z" }, + { url = "https://files.pythonhosted.org/packages/3d/94/d9a36a4ae1ed257ed5117c0905635e89327428cbf3521387c13bd85e6de1/fonttools-4.58.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2e26e4a4920d57f04bb2c3b6e9a68b099c7ef2d70881d4fee527896fa4f7b5aa", size = 2307732, upload-time = "2025-06-06T14:49:36.612Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/0f72a9fe7c051ce316779b8721c707413c53ae75ab00f970d74c7876388f/fonttools-4.58.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0bb956d9d01ea51368415515f664f58abf96557ba3c1aae4e26948ae7c86f29", size = 4718769, upload-time = "2025-06-06T14:49:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/35/dd/8be06b93e24214d7dc52fd8183dbb9e75ab9638940d84d92ced25669f4d8/fonttools-4.58.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d40af8493c80ec17a1133ef429d42f1a97258dd9213b917daae9d8cafa6e0e6c", size = 4751963, upload-time = "2025-06-06T14:49:41.391Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/85d60be364cea1b61f47bc8ea82d3e24cd6fb08640ad783fd2494bcaf4e0/fonttools-4.58.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:60b5cde1c76f6ded198da5608dddb1ee197faad7d2f0f6d3348ca0cda0c756c4", size = 4801368, upload-time = "2025-06-06T14:49:44.663Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b9/98abf9c9c1ed67eed263f091fa1bbf0ea32ef65bb8f707c2ee106b877496/fonttools-4.58.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f8df6dc80ecc9033ca25a944ee5db7564fecca28e96383043fd92d9df861a159", size = 4909670, upload-time = "2025-06-06T14:49:46.751Z" }, + { url = "https://files.pythonhosted.org/packages/32/23/d8676da27a1a27cca89549f50b4a22c98e305d9ee4c67357515d9cb25ec4/fonttools-4.58.2-cp310-cp310-win32.whl", hash = "sha256:25728e980f5fbb67f52c5311b90fae4aaec08c3d3b78dce78ab564784df1129c", size = 2191921, upload-time = "2025-06-06T14:49:48.523Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ff/ed6452dde8fd04299ec840a4fb112597a40468106039aed9abc8e35ba7eb/fonttools-4.58.2-cp310-cp310-win_amd64.whl", hash = "sha256:d6997ee7c2909a904802faf44b0d0208797c4d751f7611836011ace165308165", size = 2236374, upload-time = "2025-06-06T14:49:50.759Z" }, + { url = "https://files.pythonhosted.org/packages/63/d0/335d12ee943b8d67847864bba98478fedf3503d8b168eeeefadd8660256a/fonttools-4.58.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:024faaf20811296fd2f83ebdac7682276362e726ed5fea4062480dd36aff2fd9", size = 2755885, upload-time = "2025-06-06T14:49:52.459Z" }, + { url = "https://files.pythonhosted.org/packages/66/c2/d8ceb8b91e3847786a19d4b93749b1d804833482b5f79bee35b68327609e/fonttools-4.58.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2faec6e7f2abd80cd9f2392dfa28c02cfd5b1125be966ea6eddd6ca684deaa40", size = 2317804, upload-time = "2025-06-06T14:49:54.581Z" }, + { url = "https://files.pythonhosted.org/packages/7c/93/865c8d50b3a1f50ebdc02227f28bb81817df88cee75bc6f2652469e754b1/fonttools-4.58.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:520792629a938c14dd7fe185794b156cfc159c609d07b31bbb5f51af8dc7918a", size = 4916900, upload-time = "2025-06-06T14:49:56.366Z" }, + { url = "https://files.pythonhosted.org/packages/60/d1/301aec4f02995958b7af6728f838b2e5cc9296bec7eae350722dec31f685/fonttools-4.58.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12fbc6e0bf0c75ce475ef170f2c065be6abc9e06ad19a13b56b02ec2acf02427", size = 4937358, upload-time = "2025-06-06T14:49:58.392Z" }, + { url = "https://files.pythonhosted.org/packages/15/22/75dc23a4c7200b8feb90baa82c518684a601a3a03be25f7cc3dde1525e37/fonttools-4.58.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:44a39cf856d52109127d55576c7ec010206a8ba510161a7705021f70d1649831", size = 4980151, upload-time = "2025-06-06T14:50:00.778Z" }, + { url = "https://files.pythonhosted.org/packages/14/51/5d402f65c4b0c89ce0cdbffe86646f3996da209f7bc93f1f4a13a7211ee0/fonttools-4.58.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5390a67c55a835ad5a420da15b3d88b75412cbbd74450cb78c4916b0bd7f0a34", size = 5091255, upload-time = "2025-06-06T14:50:02.588Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5e/dee28700276129db1a0ee8ab0d5574d255a1d72df7f6df58a9d26ddef687/fonttools-4.58.2-cp311-cp311-win32.whl", hash = "sha256:f7e10f4e7160bcf6a240d7560e9e299e8cb585baed96f6a616cef51180bf56cb", size = 2190095, upload-time = "2025-06-06T14:50:04.932Z" }, + { url = "https://files.pythonhosted.org/packages/bd/60/b90fda549942808b68c1c5bada4b369f4f55d4c28a7012f7537670438f82/fonttools-4.58.2-cp311-cp311-win_amd64.whl", hash = "sha256:29bdf52bfafdae362570d3f0d3119a3b10982e1ef8cb3a9d3ebb72da81cb8d5e", size = 2238013, upload-time = "2025-06-06T14:50:06.605Z" }, + { url = "https://files.pythonhosted.org/packages/eb/68/7ec64584dc592faf944d540307c3562cd893256c48bb028c90de489e4750/fonttools-4.58.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c6eeaed9c54c1d33c1db928eb92b4e180c7cb93b50b1ee3e79b2395cb01f25e9", size = 2741645, upload-time = "2025-06-06T14:50:08.706Z" }, + { url = "https://files.pythonhosted.org/packages/8f/0c/b327838f63baa7ebdd6db3ffdf5aff638e883f9236d928be4f32c692e1bd/fonttools-4.58.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbe1d9c72b7f981bed5c2a61443d5e3127c1b3aca28ca76386d1ad93268a803f", size = 2311100, upload-time = "2025-06-06T14:50:10.401Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c7/dec024a1c873c79a4db98fe0104755fa62ec2b4518e09d6fda28246c3c9b/fonttools-4.58.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85babe5b3ce2cbe57fc0d09c0ee92bbd4d594fd7ea46a65eb43510a74a4ce773", size = 4815841, upload-time = "2025-06-06T14:50:12.496Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/57c81abad641d6ec9c8b06c99cd28d687cb4849efb6168625b5c6b8f9fa4/fonttools-4.58.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:918a2854537fcdc662938057ad58b633bc9e0698f04a2f4894258213283a7932", size = 4882659, upload-time = "2025-06-06T14:50:14.361Z" }, + { url = "https://files.pythonhosted.org/packages/a5/37/2f8faa2bf8bd1ba016ea86a94c72a5e8ef8ea1c52ec64dada617191f0515/fonttools-4.58.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b379cf05bf776c336a0205632596b1c7d7ab5f7135e3935f2ca2a0596d2d092", size = 4876128, upload-time = "2025-06-06T14:50:16.653Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ca/f1caac24ae7028a33f2a95e66c640571ff0ce5cb06c4c9ca1f632e98e22c/fonttools-4.58.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99ab3547a15a5d168c265e139e21756bbae1de04782ac9445c9ef61b8c0a32ce", size = 5027843, upload-time = "2025-06-06T14:50:18.582Z" }, + { url = "https://files.pythonhosted.org/packages/52/6e/3200fa2bafeed748a3017e4e6594751fd50cce544270919265451b21b75c/fonttools-4.58.2-cp312-cp312-win32.whl", hash = "sha256:6764e7a3188ce36eea37b477cdeca602ae62e63ae9fc768ebc176518072deb04", size = 2177374, upload-time = "2025-06-06T14:50:20.454Z" }, + { url = "https://files.pythonhosted.org/packages/55/ab/8f3e726f3f3ef3062ce9bbb615727c55beb11eea96d1f443f79cafca93ee/fonttools-4.58.2-cp312-cp312-win_amd64.whl", hash = "sha256:41f02182a1d41b79bae93c1551855146868b04ec3e7f9c57d6fef41a124e6b29", size = 2226685, upload-time = "2025-06-06T14:50:22.087Z" }, + { url = "https://files.pythonhosted.org/packages/ac/01/29f81970a508408af20b434ff5136cd1c7ef92198957eb8ddadfbb9ef177/fonttools-4.58.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:829048ef29dbefec35d95cc6811014720371c95bdc6ceb0afd2f8e407c41697c", size = 2732398, upload-time = "2025-06-06T14:50:23.821Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f1/095f2338359333adb2f1c51b8b2ad94bf9a2fa17e5fcbdf8a7b8e3672d2d/fonttools-4.58.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:64998c5993431e45b474ed5f579f18555f45309dd1cf8008b594d2fe0a94be59", size = 2306390, upload-time = "2025-06-06T14:50:25.942Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d4/9eba134c7666a26668c28945355cd86e5d57828b6b8d952a5489fe45d7e2/fonttools-4.58.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b887a1cf9fbcb920980460ee4a489c8aba7e81341f6cdaeefa08c0ab6529591c", size = 4795100, upload-time = "2025-06-06T14:50:27.653Z" }, + { url = "https://files.pythonhosted.org/packages/2a/34/345f153a24c1340daa62340c3be2d1e5ee6c1ee57e13f6d15613209e688b/fonttools-4.58.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27d74b9f6970cefbcda33609a3bee1618e5e57176c8b972134c4e22461b9c791", size = 4864585, upload-time = "2025-06-06T14:50:29.915Z" }, + { url = "https://files.pythonhosted.org/packages/01/5f/091979a25c9a6c4ba064716cfdfe9431f78ed6ffba4bd05ae01eee3532e9/fonttools-4.58.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec26784610056a770e15a60f9920cee26ae10d44d1e43271ea652dadf4e7a236", size = 4866191, upload-time = "2025-06-06T14:50:32.188Z" }, + { url = "https://files.pythonhosted.org/packages/9d/09/3944d0ece4a39560918cba37c2e0453a5f826b665a6db0b43abbd9dbe7e1/fonttools-4.58.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ed0a71d57dd427c0fb89febd08cac9b925284d2a8888e982a6c04714b82698d7", size = 5003867, upload-time = "2025-06-06T14:50:34.323Z" }, + { url = "https://files.pythonhosted.org/packages/68/97/190b8f9ba22f8b7d07df2faa9fd7087b453776d0705d3cb5b0cbd89b8ef0/fonttools-4.58.2-cp313-cp313-win32.whl", hash = "sha256:994e362b01460aa863ef0cb41a29880bc1a498c546952df465deff7abf75587a", size = 2175688, upload-time = "2025-06-06T14:50:36.211Z" }, + { url = "https://files.pythonhosted.org/packages/94/ea/0e6d4a39528dbb6e0f908c2ad219975be0a506ed440fddf5453b90f76981/fonttools-4.58.2-cp313-cp313-win_amd64.whl", hash = "sha256:f95dec862d7c395f2d4efe0535d9bdaf1e3811e51b86432fa2a77e73f8195756", size = 2226464, upload-time = "2025-06-06T14:50:38.862Z" }, + { url = "https://files.pythonhosted.org/packages/b0/04/48a35837f9a1a8251867063f1895e9887ad8f24cef64c0012d2aba5cc08e/fonttools-4.58.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f6ca4337e37d287535fd0089b4520cedc5666023fe4176a74e3415f917b570", size = 2741567, upload-time = "2025-06-06T14:50:41.26Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f5/7ce1b727c73400847efc02acc628376401732f94836e3cfa0c48c939fbb7/fonttools-4.58.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b269c7a783ec3be40809dc0dc536230a3d2d2c08e3fb9538d4e0213872b1a762", size = 2310631, upload-time = "2025-06-06T14:50:43.014Z" }, + { url = "https://files.pythonhosted.org/packages/07/e9/cdfdd3afd832916baa31cf930b53113fe09defb05f5236d94b1115a2fa5a/fonttools-4.58.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1902d9b2b84cc9485663f1a72882890cd240f4464e8443af93faa34b095a4444", size = 4702909, upload-time = "2025-06-06T14:50:44.865Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cb/57ebf7750c4e0afc2f38f269354fb0a7efce8ffdb6945bddf2e7956b9662/fonttools-4.58.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a94a00ffacbb044729c6a5b29e02bf6f0e80681e9275cd374a1d25db3061328", size = 4731988, upload-time = "2025-06-06T14:50:47.016Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ec/2857a5e1c841e0f1619bb5fab71eca8dfcf97052147deba97302535bc065/fonttools-4.58.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:25d22628f8b6b49b78666415f7cfa60c88138c24d66f3e5818d09ca001810cc5", size = 4788406, upload-time = "2025-06-06T14:50:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/4b/3f/067635fb72845bf4660086e035def415ac8c4903ca896035ef82b6abf90f/fonttools-4.58.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4bacb925a045e964a44bdeb9790b8778ce659605c7a2a39ef4f12e06c323406b", size = 4896902, upload-time = "2025-06-06T14:50:50.838Z" }, + { url = "https://files.pythonhosted.org/packages/78/bf/49fb0a95eba4592424c0f8a3b968d1850160719c76cf1c85455bcfddd02c/fonttools-4.58.2-cp39-cp39-win32.whl", hash = "sha256:eb4bc19a3ab45d2b4bb8f4f7c60e55bec53016e402af0b6ff4ef0c0129193671", size = 1470137, upload-time = "2025-06-06T14:50:53.005Z" }, + { url = "https://files.pythonhosted.org/packages/50/ad/bd7a22e9d81b03c560f9b1c4e3dfabba7c7dffe223f44faf8a9149aa6b1a/fonttools-4.58.2-cp39-cp39-win_amd64.whl", hash = "sha256:c8d16973f8ab02a5a960afe1cae4db72220ef628bf397499aba8e3caa0c10e33", size = 1514683, upload-time = "2025-06-06T14:50:55.05Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e5/c1cb8ebabb80be76d4d28995da9416816653f8f572920ab5e3d2e3ac8285/fonttools-4.58.2-py3-none-any.whl", hash = "sha256:84f4b0bcfa046254a65ee7117094b4907e22dc98097a220ef108030eb3c15596", size = 1114597, upload-time = "2025-06-06T14:50:56.619Z" }, +] + +[[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 = "h5py" +version = "3.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { 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'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/57/dfb3c5c3f1bf5f5ef2e59a22dec4ff1f3d7408b55bfcefcfb0ea69ef21c6/h5py-3.14.0.tar.gz", hash = "sha256:2372116b2e0d5d3e5e705b7f663f7c8d96fa79a4052d250484ef91d24d6a08f4", size = 424323, upload-time = "2025-06-06T14:06:15.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/89/06cbb421e01dea2e338b3154326523c05d9698f89a01f9d9b65e1ec3fb18/h5py-3.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:24df6b2622f426857bda88683b16630014588a0e4155cba44e872eb011c4eaed", size = 3332522, upload-time = "2025-06-06T14:04:13.775Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e7/6c860b002329e408348735bfd0459e7b12f712c83d357abeef3ef404eaa9/h5py-3.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ff2389961ee5872de697054dd5a033b04284afc3fb52dc51d94561ece2c10c6", size = 2831051, upload-time = "2025-06-06T14:04:18.206Z" }, + { url = "https://files.pythonhosted.org/packages/fa/cd/3dd38cdb7cc9266dc4d85f27f0261680cb62f553f1523167ad7454e32b11/h5py-3.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:016e89d3be4c44f8d5e115fab60548e518ecd9efe9fa5c5324505a90773e6f03", size = 4324677, upload-time = "2025-06-06T14:04:23.438Z" }, + { url = "https://files.pythonhosted.org/packages/b1/45/e1a754dc7cd465ba35e438e28557119221ac89b20aaebef48282654e3dc7/h5py-3.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1223b902ef0b5d90bcc8a4778218d6d6cd0f5561861611eda59fa6c52b922f4d", size = 4557272, upload-time = "2025-06-06T14:04:28.863Z" }, + { url = "https://files.pythonhosted.org/packages/5c/06/f9506c1531645829d302c420851b78bb717af808dde11212c113585fae42/h5py-3.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:852b81f71df4bb9e27d407b43071d1da330d6a7094a588efa50ef02553fa7ce4", size = 2866734, upload-time = "2025-06-06T14:04:33.5Z" }, + { url = "https://files.pythonhosted.org/packages/61/1b/ad24a8ce846cf0519695c10491e99969d9d203b9632c4fcd5004b1641c2e/h5py-3.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f30dbc58f2a0efeec6c8836c97f6c94afd769023f44e2bb0ed7b17a16ec46088", size = 3352382, upload-time = "2025-06-06T14:04:37.95Z" }, + { url = "https://files.pythonhosted.org/packages/36/5b/a066e459ca48b47cc73a5c668e9924d9619da9e3c500d9fb9c29c03858ec/h5py-3.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:543877d7f3d8f8a9828ed5df6a0b78ca3d8846244b9702e99ed0d53610b583a8", size = 2852492, upload-time = "2025-06-06T14:04:42.092Z" }, + { url = "https://files.pythonhosted.org/packages/08/0c/5e6aaf221557314bc15ba0e0da92e40b24af97ab162076c8ae009320a42b/h5py-3.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c497600c0496548810047257e36360ff551df8b59156d3a4181072eed47d8ad", size = 4298002, upload-time = "2025-06-06T14:04:47.106Z" }, + { url = "https://files.pythonhosted.org/packages/21/d4/d461649cafd5137088fb7f8e78fdc6621bb0c4ff2c090a389f68e8edc136/h5py-3.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:723a40ee6505bd354bfd26385f2dae7bbfa87655f4e61bab175a49d72ebfc06b", size = 4516618, upload-time = "2025-06-06T14:04:52.467Z" }, + { url = "https://files.pythonhosted.org/packages/db/0c/6c3f879a0f8e891625817637fad902da6e764e36919ed091dc77529004ac/h5py-3.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:d2744b520440a996f2dae97f901caa8a953afc055db4673a993f2d87d7f38713", size = 2874888, upload-time = "2025-06-06T14:04:56.95Z" }, + { url = "https://files.pythonhosted.org/packages/3e/77/8f651053c1843391e38a189ccf50df7e261ef8cd8bfd8baba0cbe694f7c3/h5py-3.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e0045115d83272090b0717c555a31398c2c089b87d212ceba800d3dc5d952e23", size = 3312740, upload-time = "2025-06-06T14:05:01.193Z" }, + { url = "https://files.pythonhosted.org/packages/ff/10/20436a6cf419b31124e59fefc78d74cb061ccb22213226a583928a65d715/h5py-3.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6da62509b7e1d71a7d110478aa25d245dd32c8d9a1daee9d2a42dba8717b047a", size = 2829207, upload-time = "2025-06-06T14:05:05.061Z" }, + { url = "https://files.pythonhosted.org/packages/3f/19/c8bfe8543bfdd7ccfafd46d8cfd96fce53d6c33e9c7921f375530ee1d39a/h5py-3.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:554ef0ced3571366d4d383427c00c966c360e178b5fb5ee5bb31a435c424db0c", size = 4708455, upload-time = "2025-06-06T14:05:11.528Z" }, + { url = "https://files.pythonhosted.org/packages/86/f9/f00de11c82c88bfc1ef22633557bfba9e271e0cb3189ad704183fc4a2644/h5py-3.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cbd41f4e3761f150aa5b662df991868ca533872c95467216f2bec5fcad84882", size = 4929422, upload-time = "2025-06-06T14:05:18.399Z" }, + { url = "https://files.pythonhosted.org/packages/7a/6d/6426d5d456f593c94b96fa942a9b3988ce4d65ebaf57d7273e452a7222e8/h5py-3.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:bf4897d67e613ecf5bdfbdab39a1158a64df105827da70ea1d90243d796d367f", size = 2862845, upload-time = "2025-06-06T14:05:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c2/7efe82d09ca10afd77cd7c286e42342d520c049a8c43650194928bcc635c/h5py-3.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:aa4b7bbce683379b7bf80aaba68e17e23396100336a8d500206520052be2f812", size = 3289245, upload-time = "2025-06-06T14:05:28.24Z" }, + { url = "https://files.pythonhosted.org/packages/4f/31/f570fab1239b0d9441024b92b6ad03bb414ffa69101a985e4c83d37608bd/h5py-3.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9603a501a04fcd0ba28dd8f0995303d26a77a980a1f9474b3417543d4c6174", size = 2807335, upload-time = "2025-06-06T14:05:31.997Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ce/3a21d87896bc7e3e9255e0ad5583ae31ae9e6b4b00e0bcb2a67e2b6acdbc/h5py-3.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8cbaf6910fa3983c46172666b0b8da7b7bd90d764399ca983236f2400436eeb", size = 4700675, upload-time = "2025-06-06T14:05:37.38Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ec/86f59025306dcc6deee5fda54d980d077075b8d9889aac80f158bd585f1b/h5py-3.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d90e6445ab7c146d7f7981b11895d70bc1dd91278a4f9f9028bc0c95e4a53f13", size = 4921632, upload-time = "2025-06-06T14:05:43.464Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6d/0084ed0b78d4fd3e7530c32491f2884140d9b06365dac8a08de726421d4a/h5py-3.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:ae18e3de237a7a830adb76aaa68ad438d85fe6e19e0d99944a3ce46b772c69b3", size = 2852929, upload-time = "2025-06-06T14:05:47.659Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ac/9ea82488c8790ee5b6ad1a807cd7dc3b9dadfece1cd0e0e369f68a7a8937/h5py-3.14.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5cc1601e78027cedfec6dd50efb4802f018551754191aeb58d948bd3ec3bd7a", size = 3345097, upload-time = "2025-06-06T14:05:51.984Z" }, + { url = "https://files.pythonhosted.org/packages/6c/bc/a172ecaaf287e3af2f837f23b470b0a2229c79555a0da9ac8b5cc5bed078/h5py-3.14.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e59d2136a8b302afd25acdf7a89b634e0eb7c66b1a211ef2d0457853768a2ef", size = 2843320, upload-time = "2025-06-06T14:05:55.754Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/b423b57696514e05aa7bb06150ef96667d0e0006cc6de7ab52c71734ab51/h5py-3.14.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:573c33ad056ac7c1ab6d567b6db9df3ffc401045e3f605736218f96c1e0490c6", size = 4326368, upload-time = "2025-06-06T14:06:00.782Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/e088f89f04fdbe57ddf9de377f857158d3daa38cf5d0fb20ef9bd489e313/h5py-3.14.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ccbe17dc187c0c64178f1a10aa274ed3a57d055117588942b8a08793cc448216", size = 4559686, upload-time = "2025-06-06T14:06:07.416Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e4/fb8032d0e5480b1db9b419b5b50737b61bb3c7187c49d809975d62129fb0/h5py-3.14.0-cp39-cp39-win_amd64.whl", hash = "sha256:4f025cf30ae738c4c4e38c7439a761a71ccfcce04c2b87b2a2ac64e8c5171d43", size = 2877166, upload-time = "2025-06-06T14:06:13.05Z" }, +] + +[[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 = "importlib-resources" +version = "6.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" }, +] + +[[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 = "ipykernel" +version = "6.29.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appnope", marker = "sys_platform == 'darwin'" }, + { name = "comm" }, + { name = "debugpy" }, + { name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "ipython", version = "9.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "matplotlib-inline" }, + { name = "nest-asyncio" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/5c/67594cb0c7055dc50814b21731c22a601101ea3b1b50a9a1b090e11f5d0f/ipykernel-6.29.5.tar.gz", hash = "sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215", size = 163367, upload-time = "2024-07-01T14:07:22.543Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/5c/368ae6c01c7628438358e6d337c19b05425727fbb221d2a3c4303c372f42/ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5", size = 117173, upload-time = "2024-07-01T14:07:19.603Z" }, +] + +[[package]] +name = "ipython" +version = "8.18.1" +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'" }, + { name = "decorator", marker = "python_full_version < '3.10'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, + { name = "jedi", marker = "python_full_version < '3.10'" }, + { name = "matplotlib-inline", marker = "python_full_version < '3.10'" }, + { name = "pexpect", marker = "python_full_version < '3.10' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version < '3.10'" }, + { name = "pygments", marker = "python_full_version < '3.10'" }, + { name = "stack-data", marker = "python_full_version < '3.10'" }, + { name = "traitlets", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/b9/3ba6c45a6df813c09a48bac313c22ff83efa26cbb55011218d925a46e2ad/ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27", size = 5486330, upload-time = "2023-11-27T09:58:34.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/6b/d9fdcdef2eb6a23f391251fde8781c38d42acd82abe84d054cb74f7863b0/ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397", size = 808161, upload-time = "2023-11-27T09:58:30.538Z" }, +] + +[[package]] +name = "ipython" +version = "8.37.0" +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'" }, + { name = "decorator", marker = "python_full_version == '3.10.*'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "jedi", marker = "python_full_version == '3.10.*'" }, + { name = "matplotlib-inline", marker = "python_full_version == '3.10.*'" }, + { name = "pexpect", marker = "python_full_version == '3.10.*' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version == '3.10.*'" }, + { name = "pygments", marker = "python_full_version == '3.10.*'" }, + { name = "stack-data", marker = "python_full_version == '3.10.*'" }, + { name = "traitlets", marker = "python_full_version == '3.10.*'" }, + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/31/10ac88f3357fc276dc8a64e8880c82e80e7459326ae1d0a211b40abf6665/ipython-8.37.0.tar.gz", hash = "sha256:ca815841e1a41a1e6b73a0b08f3038af9b2252564d01fc405356d34033012216", size = 5606088, upload-time = "2025-05-31T16:39:09.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/d0/274fbf7b0b12643cbbc001ce13e6a5b1607ac4929d1b11c72460152c9fc3/ipython-8.37.0-py3-none-any.whl", hash = "sha256:ed87326596b878932dbcb171e3e698845434d8c61b8d8cd474bf663041a9dcf2", size = 831864, upload-time = "2025-05-31T16:39:06.38Z" }, +] + +[[package]] +name = "ipython" +version = "9.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version >= '3.11'" }, + { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" }, + { name = "jedi", marker = "python_full_version >= '3.11'" }, + { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" }, + { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" }, + { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "stack-data", marker = "python_full_version >= '3.11'" }, + { name = "traitlets", marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/09/4c7e06b96fbd203e06567b60fb41b06db606b6a82db6db7b2c85bb72a15c/ipython-9.3.0.tar.gz", hash = "sha256:79eb896f9f23f50ad16c3bc205f686f6e030ad246cc309c6279a242b14afe9d8", size = 4426460, upload-time = "2025-05-31T16:34:55.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/99/9ed3d52d00f1846679e3aa12e2326ac7044b5e7f90dc822b60115fa533ca/ipython-9.3.0-py3-none-any.whl", hash = "sha256:1a0b6dd9221a1f5dddf725b57ac0cb6fddc7b5f470576231ae9162b9b3455a04", size = 605320, upload-time = "2025-05-31T16:34:52.154Z" }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, +] + +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "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 = "jupyter-client" +version = "8.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jupyter-core" }, + { name = "python-dateutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/22/bf9f12fdaeae18019a468b68952a60fe6dbab5d67cd2a103cac7659b41ca/jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419", size = 342019, upload-time = "2024-09-17T10:44:17.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f", size = 106105, upload-time = "2024-09-17T10:44:15.218Z" }, +] + +[[package]] +name = "jupyter-core" +version = "5.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "pywin32", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'win32'" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/1b/72906d554acfeb588332eaaa6f61577705e9ec752ddb486f302dafa292d9/jupyter_core-5.8.1.tar.gz", hash = "sha256:0a5f9706f70e64786b75acba995988915ebd4601c8a52e534a40b51c95f59941", size = 88923, upload-time = "2025-05-27T07:38:16.655Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/57/6bffd4b20b88da3800c5d691e0337761576ee688eb01299eae865689d2df/jupyter_core-5.8.1-py3-none-any.whl", hash = "sha256:c28d268fc90fb53f1338ded2eb410704c5449a358406e8a948b75706e24863d0", size = 28880, upload-time = "2025-05-27T07:38:15.137Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/85/4d/2255e1c76304cbd60b48cee302b66d1dde4468dc5b1160e4b7cb43778f2a/kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60", size = 97286, upload-time = "2024-09-04T09:39:44.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/14/fc943dd65268a96347472b4fbe5dcc2f6f55034516f80576cd0dd3a8930f/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6", size = 122440, upload-time = "2024-09-04T09:03:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/1e/46/e68fed66236b69dd02fcdb506218c05ac0e39745d696d22709498896875d/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17", size = 65758, upload-time = "2024-09-04T09:03:46.582Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fa/65de49c85838681fc9cb05de2a68067a683717321e01ddafb5b8024286f0/kiwisolver-1.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9", size = 64311, upload-time = "2024-09-04T09:03:47.973Z" }, + { url = "https://files.pythonhosted.org/packages/42/9c/cc8d90f6ef550f65443bad5872ffa68f3dee36de4974768628bea7c14979/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9", size = 1637109, upload-time = "2024-09-04T09:03:49.281Z" }, + { url = "https://files.pythonhosted.org/packages/55/91/0a57ce324caf2ff5403edab71c508dd8f648094b18cfbb4c8cc0fde4a6ac/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c", size = 1617814, upload-time = "2024-09-04T09:03:51.444Z" }, + { url = "https://files.pythonhosted.org/packages/12/5d/c36140313f2510e20207708adf36ae4919416d697ee0236b0ddfb6fd1050/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599", size = 1400881, upload-time = "2024-09-04T09:03:53.357Z" }, + { url = "https://files.pythonhosted.org/packages/56/d0/786e524f9ed648324a466ca8df86298780ef2b29c25313d9a4f16992d3cf/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05", size = 1512972, upload-time = "2024-09-04T09:03:55.082Z" }, + { url = "https://files.pythonhosted.org/packages/67/5a/77851f2f201e6141d63c10a0708e996a1363efaf9e1609ad0441b343763b/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407", size = 1444787, upload-time = "2024-09-04T09:03:56.588Z" }, + { url = "https://files.pythonhosted.org/packages/06/5f/1f5eaab84355885e224a6fc8d73089e8713dc7e91c121f00b9a1c58a2195/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278", size = 2199212, upload-time = "2024-09-04T09:03:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/b5/28/9152a3bfe976a0ae21d445415defc9d1cd8614b2910b7614b30b27a47270/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5", size = 2346399, upload-time = "2024-09-04T09:04:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/f6/453d1904c52ac3b400f4d5e240ac5fec25263716723e44be65f4d7149d13/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad", size = 2308688, upload-time = "2024-09-04T09:04:02.216Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9a/d4968499441b9ae187e81745e3277a8b4d7c60840a52dc9d535a7909fac3/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895", size = 2445493, upload-time = "2024-09-04T09:04:04.571Z" }, + { url = "https://files.pythonhosted.org/packages/07/c9/032267192e7828520dacb64dfdb1d74f292765f179e467c1cba97687f17d/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3", size = 2262191, upload-time = "2024-09-04T09:04:05.969Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ad/db0aedb638a58b2951da46ddaeecf204be8b4f5454df020d850c7fa8dca8/kiwisolver-1.4.7-cp310-cp310-win32.whl", hash = "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc", size = 46644, upload-time = "2024-09-04T09:04:07.408Z" }, + { url = "https://files.pythonhosted.org/packages/12/ca/d0f7b7ffbb0be1e7c2258b53554efec1fd652921f10d7d85045aff93ab61/kiwisolver-1.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c", size = 55877, upload-time = "2024-09-04T09:04:08.869Z" }, + { url = "https://files.pythonhosted.org/packages/97/6c/cfcc128672f47a3e3c0d918ecb67830600078b025bfc32d858f2e2d5c6a4/kiwisolver-1.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a", size = 48347, upload-time = "2024-09-04T09:04:10.106Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/77429fa0a58f941d6e1c58da9efe08597d2e86bf2b2cce6626834f49d07b/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54", size = 122442, upload-time = "2024-09-04T09:04:11.432Z" }, + { url = "https://files.pythonhosted.org/packages/e5/20/8c75caed8f2462d63c7fd65e16c832b8f76cda331ac9e615e914ee80bac9/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95", size = 65762, upload-time = "2024-09-04T09:04:12.468Z" }, + { url = "https://files.pythonhosted.org/packages/f4/98/fe010f15dc7230f45bc4cf367b012d651367fd203caaa992fd1f5963560e/kiwisolver-1.4.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935", size = 64319, upload-time = "2024-09-04T09:04:13.635Z" }, + { url = "https://files.pythonhosted.org/packages/8b/1b/b5d618f4e58c0675654c1e5051bcf42c776703edb21c02b8c74135541f60/kiwisolver-1.4.7-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb", size = 1334260, upload-time = "2024-09-04T09:04:14.878Z" }, + { url = "https://files.pythonhosted.org/packages/b8/01/946852b13057a162a8c32c4c8d2e9ed79f0bb5d86569a40c0b5fb103e373/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02", size = 1426589, upload-time = "2024-09-04T09:04:16.514Z" }, + { url = "https://files.pythonhosted.org/packages/70/d1/c9f96df26b459e15cf8a965304e6e6f4eb291e0f7a9460b4ad97b047561e/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51", size = 1541080, upload-time = "2024-09-04T09:04:18.322Z" }, + { url = "https://files.pythonhosted.org/packages/d3/73/2686990eb8b02d05f3de759d6a23a4ee7d491e659007dd4c075fede4b5d0/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052", size = 1470049, upload-time = "2024-09-04T09:04:20.266Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4b/2db7af3ed3af7c35f388d5f53c28e155cd402a55432d800c543dc6deb731/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18", size = 1426376, upload-time = "2024-09-04T09:04:22.419Z" }, + { url = "https://files.pythonhosted.org/packages/05/83/2857317d04ea46dc5d115f0df7e676997bbd968ced8e2bd6f7f19cfc8d7f/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545", size = 2222231, upload-time = "2024-09-04T09:04:24.526Z" }, + { url = "https://files.pythonhosted.org/packages/0d/b5/866f86f5897cd4ab6d25d22e403404766a123f138bd6a02ecb2cdde52c18/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b", size = 2368634, upload-time = "2024-09-04T09:04:25.899Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ee/73de8385403faba55f782a41260210528fe3273d0cddcf6d51648202d6d0/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36", size = 2329024, upload-time = "2024-09-04T09:04:28.523Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/cd101d8cd2cdfaa42dc06c433df17c8303d31129c9fdd16c0ea37672af91/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3", size = 2468484, upload-time = "2024-09-04T09:04:30.547Z" }, + { url = "https://files.pythonhosted.org/packages/e1/72/84f09d45a10bc57a40bb58b81b99d8f22b58b2040c912b7eb97ebf625bf2/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523", size = 2284078, upload-time = "2024-09-04T09:04:33.218Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d4/71828f32b956612dc36efd7be1788980cb1e66bfb3706e6dec9acad9b4f9/kiwisolver-1.4.7-cp311-cp311-win32.whl", hash = "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d", size = 46645, upload-time = "2024-09-04T09:04:34.371Z" }, + { url = "https://files.pythonhosted.org/packages/a1/65/d43e9a20aabcf2e798ad1aff6c143ae3a42cf506754bcb6a7ed8259c8425/kiwisolver-1.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b", size = 56022, upload-time = "2024-09-04T09:04:35.786Z" }, + { url = "https://files.pythonhosted.org/packages/35/b3/9f75a2e06f1b4ca00b2b192bc2b739334127d27f1d0625627ff8479302ba/kiwisolver-1.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376", size = 48536, upload-time = "2024-09-04T09:04:37.525Z" }, + { url = "https://files.pythonhosted.org/packages/97/9c/0a11c714cf8b6ef91001c8212c4ef207f772dd84540104952c45c1f0a249/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2", size = 121808, upload-time = "2024-09-04T09:04:38.637Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d8/0fe8c5f5d35878ddd135f44f2af0e4e1d379e1c7b0716f97cdcb88d4fd27/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a", size = 65531, upload-time = "2024-09-04T09:04:39.694Z" }, + { url = "https://files.pythonhosted.org/packages/80/c5/57fa58276dfdfa612241d640a64ca2f76adc6ffcebdbd135b4ef60095098/kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee", size = 63894, upload-time = "2024-09-04T09:04:41.6Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e9/26d3edd4c4ad1c5b891d8747a4f81b1b0aba9fb9721de6600a4adc09773b/kiwisolver-1.4.7-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640", size = 1369296, upload-time = "2024-09-04T09:04:42.886Z" }, + { url = "https://files.pythonhosted.org/packages/b6/67/3f4850b5e6cffb75ec40577ddf54f7b82b15269cc5097ff2e968ee32ea7d/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f", size = 1461450, upload-time = "2024-09-04T09:04:46.284Z" }, + { url = "https://files.pythonhosted.org/packages/52/be/86cbb9c9a315e98a8dc6b1d23c43cffd91d97d49318854f9c37b0e41cd68/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483", size = 1579168, upload-time = "2024-09-04T09:04:47.91Z" }, + { url = "https://files.pythonhosted.org/packages/0f/00/65061acf64bd5fd34c1f4ae53f20b43b0a017a541f242a60b135b9d1e301/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258", size = 1507308, upload-time = "2024-09-04T09:04:49.465Z" }, + { url = "https://files.pythonhosted.org/packages/21/e4/c0b6746fd2eb62fe702118b3ca0cb384ce95e1261cfada58ff693aeec08a/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e", size = 1464186, upload-time = "2024-09-04T09:04:50.949Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0f/529d0a9fffb4d514f2782c829b0b4b371f7f441d61aa55f1de1c614c4ef3/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107", size = 2247877, upload-time = "2024-09-04T09:04:52.388Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e1/66603ad779258843036d45adcbe1af0d1a889a07af4635f8b4ec7dccda35/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948", size = 2404204, upload-time = "2024-09-04T09:04:54.385Z" }, + { url = "https://files.pythonhosted.org/packages/8d/61/de5fb1ca7ad1f9ab7970e340a5b833d735df24689047de6ae71ab9d8d0e7/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038", size = 2352461, upload-time = "2024-09-04T09:04:56.307Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d2/0edc00a852e369827f7e05fd008275f550353f1f9bcd55db9363d779fc63/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383", size = 2501358, upload-time = "2024-09-04T09:04:57.922Z" }, + { url = "https://files.pythonhosted.org/packages/84/15/adc15a483506aec6986c01fb7f237c3aec4d9ed4ac10b756e98a76835933/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520", size = 2314119, upload-time = "2024-09-04T09:04:59.332Z" }, + { url = "https://files.pythonhosted.org/packages/36/08/3a5bb2c53c89660863a5aa1ee236912269f2af8762af04a2e11df851d7b2/kiwisolver-1.4.7-cp312-cp312-win32.whl", hash = "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b", size = 46367, upload-time = "2024-09-04T09:05:00.804Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/c05f0a6d825c643779fc3c70876bff1ac221f0e31e6f701f0e9578690d70/kiwisolver-1.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb", size = 55884, upload-time = "2024-09-04T09:05:01.924Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f9/3828d8f21b6de4279f0667fb50a9f5215e6fe57d5ec0d61905914f5b6099/kiwisolver-1.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a", size = 48528, upload-time = "2024-09-04T09:05:02.983Z" }, + { url = "https://files.pythonhosted.org/packages/c4/06/7da99b04259b0f18b557a4effd1b9c901a747f7fdd84cf834ccf520cb0b2/kiwisolver-1.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2e6039dcbe79a8e0f044f1c39db1986a1b8071051efba3ee4d74f5b365f5226e", size = 121913, upload-time = "2024-09-04T09:05:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/97/f5/b8a370d1aa593c17882af0a6f6755aaecd643640c0ed72dcfd2eafc388b9/kiwisolver-1.4.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a1ecf0ac1c518487d9d23b1cd7139a6a65bc460cd101ab01f1be82ecf09794b6", size = 65627, upload-time = "2024-09-04T09:05:05.119Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fc/6c0374f7503522539e2d4d1b497f5ebad3f8ed07ab51aed2af988dd0fb65/kiwisolver-1.4.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ab9ccab2b5bd5702ab0803676a580fffa2aa178c2badc5557a84cc943fcf750", size = 63888, upload-time = "2024-09-04T09:05:06.191Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3e/0b7172793d0f41cae5c923492da89a2ffcd1adf764c16159ca047463ebd3/kiwisolver-1.4.7-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f816dd2277f8d63d79f9c8473a79fe54047bc0467754962840782c575522224d", size = 1369145, upload-time = "2024-09-04T09:05:07.919Z" }, + { url = "https://files.pythonhosted.org/packages/77/92/47d050d6f6aced2d634258123f2688fbfef8ded3c5baf2c79d94d91f1f58/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf8bcc23ceb5a1b624572a1623b9f79d2c3b337c8c455405ef231933a10da379", size = 1461448, upload-time = "2024-09-04T09:05:10.01Z" }, + { url = "https://files.pythonhosted.org/packages/9c/1b/8f80b18e20b3b294546a1adb41701e79ae21915f4175f311a90d042301cf/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dea0bf229319828467d7fca8c7c189780aa9ff679c94539eed7532ebe33ed37c", size = 1578750, upload-time = "2024-09-04T09:05:11.598Z" }, + { url = "https://files.pythonhosted.org/packages/a4/fe/fe8e72f3be0a844f257cadd72689c0848c6d5c51bc1d60429e2d14ad776e/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c06a4c7cf15ec739ce0e5971b26c93638730090add60e183530d70848ebdd34", size = 1507175, upload-time = "2024-09-04T09:05:13.22Z" }, + { url = "https://files.pythonhosted.org/packages/39/fa/cdc0b6105d90eadc3bee525fecc9179e2b41e1ce0293caaf49cb631a6aaf/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:913983ad2deb14e66d83c28b632fd35ba2b825031f2fa4ca29675e665dfecbe1", size = 1463963, upload-time = "2024-09-04T09:05:15.925Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5c/0c03c4e542720c6177d4f408e56d1c8315899db72d46261a4e15b8b33a41/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5337ec7809bcd0f424c6b705ecf97941c46279cf5ed92311782c7c9c2026f07f", size = 2248220, upload-time = "2024-09-04T09:05:17.434Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ee/55ef86d5a574f4e767df7da3a3a7ff4954c996e12d4fbe9c408170cd7dcc/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c26ed10c4f6fa6ddb329a5120ba3b6db349ca192ae211e882970bfc9d91420b", size = 2404463, upload-time = "2024-09-04T09:05:18.997Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6d/73ad36170b4bff4825dc588acf4f3e6319cb97cd1fb3eb04d9faa6b6f212/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c619b101e6de2222c1fcb0531e1b17bbffbe54294bfba43ea0d411d428618c27", size = 2352842, upload-time = "2024-09-04T09:05:21.299Z" }, + { url = "https://files.pythonhosted.org/packages/0b/16/fa531ff9199d3b6473bb4d0f47416cdb08d556c03b8bc1cccf04e756b56d/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:073a36c8273647592ea332e816e75ef8da5c303236ec0167196793eb1e34657a", size = 2501635, upload-time = "2024-09-04T09:05:23.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/7e/aa9422e78419db0cbe75fb86d8e72b433818f2e62e2e394992d23d23a583/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3ce6b2b0231bda412463e152fc18335ba32faf4e8c23a754ad50ffa70e4091ee", size = 2314556, upload-time = "2024-09-04T09:05:25.907Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b2/15f7f556df0a6e5b3772a1e076a9d9f6c538ce5f05bd590eca8106508e06/kiwisolver-1.4.7-cp313-cp313-win32.whl", hash = "sha256:f4c9aee212bc89d4e13f58be11a56cc8036cabad119259d12ace14b34476fd07", size = 46364, upload-time = "2024-09-04T09:05:27.184Z" }, + { url = "https://files.pythonhosted.org/packages/0b/db/32e897e43a330eee8e4770bfd2737a9584b23e33587a0812b8e20aac38f7/kiwisolver-1.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:8a3ec5aa8e38fc4c8af308917ce12c536f1c88452ce554027e55b22cbbfbff76", size = 55887, upload-time = "2024-09-04T09:05:28.372Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a4/df2bdca5270ca85fd25253049eb6708d4127be2ed0e5c2650217450b59e9/kiwisolver-1.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:76c8094ac20ec259471ac53e774623eb62e6e1f56cd8690c67ce6ce4fcb05650", size = 48530, upload-time = "2024-09-04T09:05:30.225Z" }, + { url = "https://files.pythonhosted.org/packages/11/88/37ea0ea64512997b13d69772db8dcdc3bfca5442cda3a5e4bb943652ee3e/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd", size = 122449, upload-time = "2024-09-04T09:05:55.311Z" }, + { url = "https://files.pythonhosted.org/packages/4e/45/5a5c46078362cb3882dcacad687c503089263c017ca1241e0483857791eb/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583", size = 65757, upload-time = "2024-09-04T09:05:56.906Z" }, + { url = "https://files.pythonhosted.org/packages/8a/be/a6ae58978772f685d48dd2e84460937761c53c4bbd84e42b0336473d9775/kiwisolver-1.4.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417", size = 64312, upload-time = "2024-09-04T09:05:58.384Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/18ef6f452d311e1e1eb180c9bf5589187fa1f042db877e6fe443ef10099c/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904", size = 1626966, upload-time = "2024-09-04T09:05:59.855Z" }, + { url = "https://files.pythonhosted.org/packages/21/b1/40655f6c3fa11ce740e8a964fa8e4c0479c87d6a7944b95af799c7a55dfe/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a", size = 1607044, upload-time = "2024-09-04T09:06:02.16Z" }, + { url = "https://files.pythonhosted.org/packages/fd/93/af67dbcfb9b3323bbd2c2db1385a7139d8f77630e4a37bb945b57188eb2d/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8", size = 1391879, upload-time = "2024-09-04T09:06:03.908Z" }, + { url = "https://files.pythonhosted.org/packages/40/6f/d60770ef98e77b365d96061d090c0cd9e23418121c55fff188fa4bdf0b54/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2", size = 1504751, upload-time = "2024-09-04T09:06:05.58Z" }, + { url = "https://files.pythonhosted.org/packages/fa/3a/5f38667d313e983c432f3fcd86932177519ed8790c724e07d77d1de0188a/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88", size = 1436990, upload-time = "2024-09-04T09:06:08.126Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/1520301a47326e6a6043b502647e42892be33b3f051e9791cc8bb43f1a32/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde", size = 2191122, upload-time = "2024-09-04T09:06:10.345Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c4/eb52da300c166239a2233f1f9c4a1b767dfab98fae27681bfb7ea4873cb6/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c", size = 2338126, upload-time = "2024-09-04T09:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/1a/cb/42b92fd5eadd708dd9107c089e817945500685f3437ce1fd387efebc6d6e/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2", size = 2298313, upload-time = "2024-09-04T09:06:14.562Z" }, + { url = "https://files.pythonhosted.org/packages/4f/eb/be25aa791fe5fc75a8b1e0c965e00f942496bc04635c9aae8035f6b76dcd/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb", size = 2437784, upload-time = "2024-09-04T09:06:16.767Z" }, + { url = "https://files.pythonhosted.org/packages/c5/22/30a66be7f3368d76ff95689e1c2e28d382383952964ab15330a15d8bfd03/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327", size = 2253988, upload-time = "2024-09-04T09:06:18.705Z" }, + { url = "https://files.pythonhosted.org/packages/35/d3/5f2ecb94b5211c8a04f218a76133cc8d6d153b0f9cd0b45fad79907f0689/kiwisolver-1.4.7-cp39-cp39-win32.whl", hash = "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644", size = 46980, upload-time = "2024-09-04T09:06:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/ef/17/cd10d020578764ea91740204edc6b3236ed8106228a46f568d716b11feb2/kiwisolver-1.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4", size = 55847, upload-time = "2024-09-04T09:06:21.407Z" }, + { url = "https://files.pythonhosted.org/packages/91/84/32232502020bd78d1d12be7afde15811c64a95ed1f606c10456db4e4c3ac/kiwisolver-1.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f", size = 48494, upload-time = "2024-09-04T09:06:22.648Z" }, + { url = "https://files.pythonhosted.org/packages/ac/59/741b79775d67ab67ced9bb38552da688c0305c16e7ee24bba7a2be253fb7/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643", size = 59491, upload-time = "2024-09-04T09:06:24.188Z" }, + { url = "https://files.pythonhosted.org/packages/58/cc/fb239294c29a5656e99e3527f7369b174dd9cc7c3ef2dea7cb3c54a8737b/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706", size = 57648, upload-time = "2024-09-04T09:06:25.559Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ef/2f009ac1f7aab9f81efb2d837301d255279d618d27b6015780115ac64bdd/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6", size = 84257, upload-time = "2024-09-04T09:06:27.038Z" }, + { url = "https://files.pythonhosted.org/packages/81/e1/c64f50987f85b68b1c52b464bb5bf73e71570c0f7782d626d1eb283ad620/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2", size = 80906, upload-time = "2024-09-04T09:06:28.48Z" }, + { url = "https://files.pythonhosted.org/packages/fd/71/1687c5c0a0be2cee39a5c9c389e546f9c6e215e46b691d00d9f646892083/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4", size = 79951, upload-time = "2024-09-04T09:06:29.966Z" }, + { url = "https://files.pythonhosted.org/packages/ea/8b/d7497df4a1cae9367adf21665dd1f896c2a7aeb8769ad77b662c5e2bcce7/kiwisolver-1.4.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a", size = 55715, upload-time = "2024-09-04T09:06:31.489Z" }, + { url = "https://files.pythonhosted.org/packages/d5/df/ce37d9b26f07ab90880923c94d12a6ff4d27447096b4c849bfc4339ccfdf/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39", size = 58666, upload-time = "2024-09-04T09:06:43.756Z" }, + { url = "https://files.pythonhosted.org/packages/b0/d3/e4b04f43bc629ac8e186b77b2b1a251cdfa5b7610fa189dc0db622672ce6/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e", size = 57088, upload-time = "2024-09-04T09:06:45.406Z" }, + { url = "https://files.pythonhosted.org/packages/30/1c/752df58e2d339e670a535514d2db4fe8c842ce459776b8080fbe08ebb98e/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608", size = 84321, upload-time = "2024-09-04T09:06:47.557Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f8/fe6484e847bc6e238ec9f9828089fb2c0bb53f2f5f3a79351fde5b565e4f/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674", size = 80776, upload-time = "2024-09-04T09:06:49.235Z" }, + { url = "https://files.pythonhosted.org/packages/9b/57/d7163c0379f250ef763aba85330a19feefb5ce6cb541ade853aaba881524/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225", size = 79984, upload-time = "2024-09-04T09:06:51.336Z" }, + { url = "https://files.pythonhosted.org/packages/8c/95/4a103776c265d13b3d2cd24fb0494d4e04ea435a8ef97e1b2c026d43250b/kiwisolver-1.4.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0", size = 55811, upload-time = "2024-09-04T09:06:53.078Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538, upload-time = "2024-12-24T18:30:51.519Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/5f/4d8e9e852d98ecd26cdf8eaf7ed8bc33174033bba5e07001b289f07308fd/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db", size = 124623, upload-time = "2024-12-24T18:28:17.687Z" }, + { url = "https://files.pythonhosted.org/packages/1d/70/7f5af2a18a76fe92ea14675f8bd88ce53ee79e37900fa5f1a1d8e0b42998/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b", size = 66720, upload-time = "2024-12-24T18:28:19.158Z" }, + { url = "https://files.pythonhosted.org/packages/c6/13/e15f804a142353aefd089fadc8f1d985561a15358c97aca27b0979cb0785/kiwisolver-1.4.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce2cf1e5688edcb727fdf7cd1bbd0b6416758996826a8be1d958f91880d0809d", size = 65413, upload-time = "2024-12-24T18:28:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/ce/6d/67d36c4d2054e83fb875c6b59d0809d5c530de8148846b1370475eeeece9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c8bf637892dc6e6aad2bc6d4d69d08764166e5e3f69d469e55427b6ac001b19d", size = 1650826, upload-time = "2024-12-24T18:28:21.203Z" }, + { url = "https://files.pythonhosted.org/packages/de/c6/7b9bb8044e150d4d1558423a1568e4f227193662a02231064e3824f37e0a/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:034d2c891f76bd3edbdb3ea11140d8510dca675443da7304205a2eaa45d8334c", size = 1628231, upload-time = "2024-12-24T18:28:23.851Z" }, + { url = "https://files.pythonhosted.org/packages/b6/38/ad10d437563063eaaedbe2c3540a71101fc7fb07a7e71f855e93ea4de605/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47b28d1dfe0793d5e96bce90835e17edf9a499b53969b03c6c47ea5985844c3", size = 1408938, upload-time = "2024-12-24T18:28:26.687Z" }, + { url = "https://files.pythonhosted.org/packages/52/ce/c0106b3bd7f9e665c5f5bc1e07cc95b5dabd4e08e3dad42dbe2faad467e7/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb158fe28ca0c29f2260cca8c43005329ad58452c36f0edf298204de32a9a3ed", size = 1422799, upload-time = "2024-12-24T18:28:30.538Z" }, + { url = "https://files.pythonhosted.org/packages/d0/87/efb704b1d75dc9758087ba374c0f23d3254505edaedd09cf9d247f7878b9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5536185fce131780ebd809f8e623bf4030ce1b161353166c49a3c74c287897f", size = 1354362, upload-time = "2024-12-24T18:28:32.943Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b3/fd760dc214ec9a8f208b99e42e8f0130ff4b384eca8b29dd0efc62052176/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:369b75d40abedc1da2c1f4de13f3482cb99e3237b38726710f4a793432b1c5ff", size = 2222695, upload-time = "2024-12-24T18:28:35.641Z" }, + { url = "https://files.pythonhosted.org/packages/a2/09/a27fb36cca3fc01700687cc45dae7a6a5f8eeb5f657b9f710f788748e10d/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:641f2ddf9358c80faa22e22eb4c9f54bd3f0e442e038728f500e3b978d00aa7d", size = 2370802, upload-time = "2024-12-24T18:28:38.357Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c3/ba0a0346db35fe4dc1f2f2cf8b99362fbb922d7562e5f911f7ce7a7b60fa/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d561d2d8883e0819445cfe58d7ddd673e4015c3c57261d7bdcd3710d0d14005c", size = 2334646, upload-time = "2024-12-24T18:28:40.941Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/942cf69e562f5ed253ac67d5c92a693745f0bed3c81f49fc0cbebe4d6b00/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1732e065704b47c9afca7ffa272f845300a4eb959276bf6970dc07265e73b605", size = 2467260, upload-time = "2024-12-24T18:28:42.273Z" }, + { url = "https://files.pythonhosted.org/packages/32/26/2d9668f30d8a494b0411d4d7d4ea1345ba12deb6a75274d58dd6ea01e951/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bcb1ebc3547619c3b58a39e2448af089ea2ef44b37988caf432447374941574e", size = 2288633, upload-time = "2024-12-24T18:28:44.87Z" }, + { url = "https://files.pythonhosted.org/packages/98/99/0dd05071654aa44fe5d5e350729961e7bb535372935a45ac89a8924316e6/kiwisolver-1.4.8-cp310-cp310-win_amd64.whl", hash = "sha256:89c107041f7b27844179ea9c85d6da275aa55ecf28413e87624d033cf1f6b751", size = 71885, upload-time = "2024-12-24T18:28:47.346Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fc/822e532262a97442989335394d441cd1d0448c2e46d26d3e04efca84df22/kiwisolver-1.4.8-cp310-cp310-win_arm64.whl", hash = "sha256:b5773efa2be9eb9fcf5415ea3ab70fc785d598729fd6057bea38d539ead28271", size = 65175, upload-time = "2024-12-24T18:28:49.651Z" }, + { url = "https://files.pythonhosted.org/packages/da/ed/c913ee28936c371418cb167b128066ffb20bbf37771eecc2c97edf8a6e4c/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84", size = 124635, upload-time = "2024-12-24T18:28:51.826Z" }, + { url = "https://files.pythonhosted.org/packages/4c/45/4a7f896f7467aaf5f56ef093d1f329346f3b594e77c6a3c327b2d415f521/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561", size = 66717, upload-time = "2024-12-24T18:28:54.256Z" }, + { url = "https://files.pythonhosted.org/packages/5f/b4/c12b3ac0852a3a68f94598d4c8d569f55361beef6159dce4e7b624160da2/kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7", size = 65413, upload-time = "2024-12-24T18:28:55.184Z" }, + { url = "https://files.pythonhosted.org/packages/a9/98/1df4089b1ed23d83d410adfdc5947245c753bddfbe06541c4aae330e9e70/kiwisolver-1.4.8-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03", size = 1343994, upload-time = "2024-12-24T18:28:57.493Z" }, + { url = "https://files.pythonhosted.org/packages/8d/bf/b4b169b050c8421a7c53ea1ea74e4ef9c335ee9013216c558a047f162d20/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954", size = 1434804, upload-time = "2024-12-24T18:29:00.077Z" }, + { url = "https://files.pythonhosted.org/packages/66/5a/e13bd341fbcf73325ea60fdc8af752addf75c5079867af2e04cc41f34434/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79", size = 1450690, upload-time = "2024-12-24T18:29:01.401Z" }, + { url = "https://files.pythonhosted.org/packages/9b/4f/5955dcb376ba4a830384cc6fab7d7547bd6759fe75a09564910e9e3bb8ea/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6", size = 1376839, upload-time = "2024-12-24T18:29:02.685Z" }, + { url = "https://files.pythonhosted.org/packages/3a/97/5edbed69a9d0caa2e4aa616ae7df8127e10f6586940aa683a496c2c280b9/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0", size = 1435109, upload-time = "2024-12-24T18:29:04.113Z" }, + { url = "https://files.pythonhosted.org/packages/13/fc/e756382cb64e556af6c1809a1bbb22c141bbc2445049f2da06b420fe52bf/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab", size = 2245269, upload-time = "2024-12-24T18:29:05.488Z" }, + { url = "https://files.pythonhosted.org/packages/76/15/e59e45829d7f41c776d138245cabae6515cb4eb44b418f6d4109c478b481/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc", size = 2393468, upload-time = "2024-12-24T18:29:06.79Z" }, + { url = "https://files.pythonhosted.org/packages/e9/39/483558c2a913ab8384d6e4b66a932406f87c95a6080112433da5ed668559/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25", size = 2355394, upload-time = "2024-12-24T18:29:08.24Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/efad1fbca6570a161d29224f14b082960c7e08268a133fe5dc0f6906820e/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc", size = 2490901, upload-time = "2024-12-24T18:29:09.653Z" }, + { url = "https://files.pythonhosted.org/packages/c9/4f/15988966ba46bcd5ab9d0c8296914436720dd67fca689ae1a75b4ec1c72f/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67", size = 2312306, upload-time = "2024-12-24T18:29:12.644Z" }, + { url = "https://files.pythonhosted.org/packages/2d/27/bdf1c769c83f74d98cbc34483a972f221440703054894a37d174fba8aa68/kiwisolver-1.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34", size = 71966, upload-time = "2024-12-24T18:29:14.089Z" }, + { url = "https://files.pythonhosted.org/packages/4a/c9/9642ea855604aeb2968a8e145fc662edf61db7632ad2e4fb92424be6b6c0/kiwisolver-1.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2", size = 65311, upload-time = "2024-12-24T18:29:15.892Z" }, + { url = "https://files.pythonhosted.org/packages/fc/aa/cea685c4ab647f349c3bc92d2daf7ae34c8e8cf405a6dcd3a497f58a2ac3/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502", size = 124152, upload-time = "2024-12-24T18:29:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/c5/0b/8db6d2e2452d60d5ebc4ce4b204feeb16176a851fd42462f66ade6808084/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31", size = 66555, upload-time = "2024-12-24T18:29:19.146Z" }, + { url = "https://files.pythonhosted.org/packages/60/26/d6a0db6785dd35d3ba5bf2b2df0aedc5af089962c6eb2cbf67a15b81369e/kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb", size = 65067, upload-time = "2024-12-24T18:29:20.096Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ed/1d97f7e3561e09757a196231edccc1bcf59d55ddccefa2afc9c615abd8e0/kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f", size = 1378443, upload-time = "2024-12-24T18:29:22.843Z" }, + { url = "https://files.pythonhosted.org/packages/29/61/39d30b99954e6b46f760e6289c12fede2ab96a254c443639052d1b573fbc/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc", size = 1472728, upload-time = "2024-12-24T18:29:24.463Z" }, + { url = "https://files.pythonhosted.org/packages/0c/3e/804163b932f7603ef256e4a715e5843a9600802bb23a68b4e08c8c0ff61d/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a", size = 1478388, upload-time = "2024-12-24T18:29:25.776Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9e/60eaa75169a154700be74f875a4d9961b11ba048bef315fbe89cb6999056/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a", size = 1413849, upload-time = "2024-12-24T18:29:27.202Z" }, + { url = "https://files.pythonhosted.org/packages/bc/b3/9458adb9472e61a998c8c4d95cfdfec91c73c53a375b30b1428310f923e4/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a", size = 1475533, upload-time = "2024-12-24T18:29:28.638Z" }, + { url = "https://files.pythonhosted.org/packages/e4/7a/0a42d9571e35798de80aef4bb43a9b672aa7f8e58643d7bd1950398ffb0a/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3", size = 2268898, upload-time = "2024-12-24T18:29:30.368Z" }, + { url = "https://files.pythonhosted.org/packages/d9/07/1255dc8d80271400126ed8db35a1795b1a2c098ac3a72645075d06fe5c5d/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b", size = 2425605, upload-time = "2024-12-24T18:29:33.151Z" }, + { url = "https://files.pythonhosted.org/packages/84/df/5a3b4cf13780ef6f6942df67b138b03b7e79e9f1f08f57c49957d5867f6e/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4", size = 2375801, upload-time = "2024-12-24T18:29:34.584Z" }, + { url = "https://files.pythonhosted.org/packages/8f/10/2348d068e8b0f635c8c86892788dac7a6b5c0cb12356620ab575775aad89/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d", size = 2520077, upload-time = "2024-12-24T18:29:36.138Z" }, + { url = "https://files.pythonhosted.org/packages/32/d8/014b89fee5d4dce157d814303b0fce4d31385a2af4c41fed194b173b81ac/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8", size = 2338410, upload-time = "2024-12-24T18:29:39.991Z" }, + { url = "https://files.pythonhosted.org/packages/bd/72/dfff0cc97f2a0776e1c9eb5bef1ddfd45f46246c6533b0191887a427bca5/kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50", size = 71853, upload-time = "2024-12-24T18:29:42.006Z" }, + { url = "https://files.pythonhosted.org/packages/dc/85/220d13d914485c0948a00f0b9eb419efaf6da81b7d72e88ce2391f7aed8d/kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476", size = 65424, upload-time = "2024-12-24T18:29:44.38Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/e62464a652f4f8cd9006e13d07abad844a47df1e6537f73ddfbf1bc997ec/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09", size = 124156, upload-time = "2024-12-24T18:29:45.368Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2d/f13d06998b546a2ad4f48607a146e045bbe48030774de29f90bdc573df15/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1", size = 66555, upload-time = "2024-12-24T18:29:46.37Z" }, + { url = "https://files.pythonhosted.org/packages/59/e3/b8bd14b0a54998a9fd1e8da591c60998dc003618cb19a3f94cb233ec1511/kiwisolver-1.4.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c", size = 65071, upload-time = "2024-12-24T18:29:47.333Z" }, + { url = "https://files.pythonhosted.org/packages/f0/1c/6c86f6d85ffe4d0ce04228d976f00674f1df5dc893bf2dd4f1928748f187/kiwisolver-1.4.8-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b", size = 1378053, upload-time = "2024-12-24T18:29:49.636Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b9/1c6e9f6dcb103ac5cf87cb695845f5fa71379021500153566d8a8a9fc291/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47", size = 1472278, upload-time = "2024-12-24T18:29:51.164Z" }, + { url = "https://files.pythonhosted.org/packages/ee/81/aca1eb176de671f8bda479b11acdc42c132b61a2ac861c883907dde6debb/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16", size = 1478139, upload-time = "2024-12-24T18:29:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/49/f4/e081522473671c97b2687d380e9e4c26f748a86363ce5af48b4a28e48d06/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc", size = 1413517, upload-time = "2024-12-24T18:29:53.941Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e9/6a7d025d8da8c4931522922cd706105aa32b3291d1add8c5427cdcd66e63/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246", size = 1474952, upload-time = "2024-12-24T18:29:56.523Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/13fa685ae167bee5d94b415991c4fc7bb0a1b6ebea6e753a87044b209678/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794", size = 2269132, upload-time = "2024-12-24T18:29:57.989Z" }, + { url = "https://files.pythonhosted.org/packages/ef/92/bb7c9395489b99a6cb41d502d3686bac692586db2045adc19e45ee64ed23/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b", size = 2425997, upload-time = "2024-12-24T18:29:59.393Z" }, + { url = "https://files.pythonhosted.org/packages/ed/12/87f0e9271e2b63d35d0d8524954145837dd1a6c15b62a2d8c1ebe0f182b4/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3", size = 2376060, upload-time = "2024-12-24T18:30:01.338Z" }, + { url = "https://files.pythonhosted.org/packages/02/6e/c8af39288edbce8bf0fa35dee427b082758a4b71e9c91ef18fa667782138/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957", size = 2520471, upload-time = "2024-12-24T18:30:04.574Z" }, + { url = "https://files.pythonhosted.org/packages/13/78/df381bc7b26e535c91469f77f16adcd073beb3e2dd25042efd064af82323/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb", size = 2338793, upload-time = "2024-12-24T18:30:06.25Z" }, + { url = "https://files.pythonhosted.org/packages/d0/dc/c1abe38c37c071d0fc71c9a474fd0b9ede05d42f5a458d584619cfd2371a/kiwisolver-1.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2", size = 71855, upload-time = "2024-12-24T18:30:07.535Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b6/21529d595b126ac298fdd90b705d87d4c5693de60023e0efcb4f387ed99e/kiwisolver-1.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30", size = 65430, upload-time = "2024-12-24T18:30:08.504Z" }, + { url = "https://files.pythonhosted.org/packages/34/bd/b89380b7298e3af9b39f49334e3e2a4af0e04819789f04b43d560516c0c8/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c", size = 126294, upload-time = "2024-12-24T18:30:09.508Z" }, + { url = "https://files.pythonhosted.org/packages/83/41/5857dc72e5e4148eaac5aa76e0703e594e4465f8ab7ec0fc60e3a9bb8fea/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc", size = 67736, upload-time = "2024-12-24T18:30:11.039Z" }, + { url = "https://files.pythonhosted.org/packages/e1/d1/be059b8db56ac270489fb0b3297fd1e53d195ba76e9bbb30e5401fa6b759/kiwisolver-1.4.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712", size = 66194, upload-time = "2024-12-24T18:30:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e1/83/4b73975f149819eb7dcf9299ed467eba068ecb16439a98990dcb12e63fdd/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e", size = 1465942, upload-time = "2024-12-24T18:30:18.927Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2c/30a5cdde5102958e602c07466bce058b9d7cb48734aa7a4327261ac8e002/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880", size = 1595341, upload-time = "2024-12-24T18:30:22.102Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9b/1e71db1c000385aa069704f5990574b8244cce854ecd83119c19e83c9586/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062", size = 1598455, upload-time = "2024-12-24T18:30:24.947Z" }, + { url = "https://files.pythonhosted.org/packages/85/92/c8fec52ddf06231b31cbb779af77e99b8253cd96bd135250b9498144c78b/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7", size = 1522138, upload-time = "2024-12-24T18:30:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/0b/51/9eb7e2cd07a15d8bdd976f6190c0164f92ce1904e5c0c79198c4972926b7/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed", size = 1582857, upload-time = "2024-12-24T18:30:28.86Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/c5a00387a5405e68ba32cc64af65ce881a39b98d73cc394b24143bebc5b8/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d", size = 2293129, upload-time = "2024-12-24T18:30:30.34Z" }, + { url = "https://files.pythonhosted.org/packages/44/83/eeb7af7d706b8347548313fa3a3a15931f404533cc54fe01f39e830dd231/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165", size = 2421538, upload-time = "2024-12-24T18:30:33.334Z" }, + { url = "https://files.pythonhosted.org/packages/05/f9/27e94c1b3eb29e6933b6986ffc5fa1177d2cd1f0c8efc5f02c91c9ac61de/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6", size = 2390661, upload-time = "2024-12-24T18:30:34.939Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d4/3c9735faa36ac591a4afcc2980d2691000506050b7a7e80bcfe44048daa7/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90", size = 2546710, upload-time = "2024-12-24T18:30:37.281Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fa/be89a49c640930180657482a74970cdcf6f7072c8d2471e1babe17a222dc/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85", size = 2349213, upload-time = "2024-12-24T18:30:40.019Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f9/ae81c47a43e33b93b0a9819cac6723257f5da2a5a60daf46aa5c7226ea85/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e7a019419b7b510f0f7c9dceff8c5eae2392037eae483a7f9162625233802b0a", size = 60403, upload-time = "2024-12-24T18:30:41.372Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/f92b5cb6f4ce0c1ebfcfe3e2e42b96917e16f7090e45b21102941924f18f/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:286b18e86682fd2217a48fc6be6b0f20c1d0ed10958d8dc53453ad58d7be0bf8", size = 58657, upload-time = "2024-12-24T18:30:42.392Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/ae0240f732f0484d3a4dc885d055653c47144bdf59b670aae0ec3c65a7c8/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4191ee8dfd0be1c3666ccbac178c5a05d5f8d689bbe3fc92f3c4abec817f8fe0", size = 84948, upload-time = "2024-12-24T18:30:44.703Z" }, + { url = "https://files.pythonhosted.org/packages/5d/eb/78d50346c51db22c7203c1611f9b513075f35c4e0e4877c5dde378d66043/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd2785b9391f2873ad46088ed7599a6a71e762e1ea33e87514b1a441ed1da1c", size = 81186, upload-time = "2024-12-24T18:30:45.654Z" }, + { url = "https://files.pythonhosted.org/packages/43/f8/7259f18c77adca88d5f64f9a522792e178b2691f3748817a8750c2d216ef/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c07b29089b7ba090b6f1a669f1411f27221c3662b3a1b7010e67b59bb5a6f10b", size = 80279, upload-time = "2024-12-24T18:30:47.951Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1d/50ad811d1c5dae091e4cf046beba925bcae0a610e79ae4c538f996f63ed5/kiwisolver-1.4.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:65ea09a5a3faadd59c2ce96dc7bf0f364986a315949dc6374f04396b0d60e09b", size = 71762, upload-time = "2024-12-24T18:30:48.903Z" }, +] + +[[package]] +name = "llvmlite" +version = "0.43.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/3d/f513755f285db51ab363a53e898b85562e950f79a2e6767a364530c2f645/llvmlite-0.43.0.tar.gz", hash = "sha256:ae2b5b5c3ef67354824fb75517c8db5fbe93bc02cd9671f3c62271626bc041d5", size = 157069, upload-time = "2024-06-13T18:09:32.641Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/ff/6ca7e98998b573b4bd6566f15c35e5c8bea829663a6df0c7aa55ab559da9/llvmlite-0.43.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a289af9a1687c6cf463478f0fa8e8aa3b6fb813317b0d70bf1ed0759eab6f761", size = 31064408, upload-time = "2024-06-13T18:08:13.462Z" }, + { url = "https://files.pythonhosted.org/packages/ca/5c/a27f9257f86f0cda3f764ff21d9f4217b9f6a0d45e7a39ecfa7905f524ce/llvmlite-0.43.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d4fd101f571a31acb1559ae1af30f30b1dc4b3186669f92ad780e17c81e91bc", size = 28793153, upload-time = "2024-06-13T18:08:17.336Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3c/4410f670ad0a911227ea2ecfcba9f672a77cf1924df5280c4562032ec32d/llvmlite-0.43.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d434ec7e2ce3cc8f452d1cd9a28591745de022f931d67be688a737320dfcead", size = 42857276, upload-time = "2024-06-13T18:08:21.071Z" }, + { url = "https://files.pythonhosted.org/packages/c6/21/2ffbab5714e72f2483207b4a1de79b2eecd9debbf666ff4e7067bcc5c134/llvmlite-0.43.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6912a87782acdff6eb8bf01675ed01d60ca1f2551f8176a300a886f09e836a6a", size = 43871781, upload-time = "2024-06-13T18:08:26.32Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/b5478037c453554a61625ef1125f7e12bb1429ae11c6376f47beba9b0179/llvmlite-0.43.0-cp310-cp310-win_amd64.whl", hash = "sha256:14f0e4bf2fd2d9a75a3534111e8ebeb08eda2f33e9bdd6dfa13282afacdde0ed", size = 28123487, upload-time = "2024-06-13T18:08:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/95/8c/de3276d773ab6ce3ad676df5fab5aac19696b2956319d65d7dd88fb10f19/llvmlite-0.43.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3e8d0618cb9bfe40ac38a9633f2493d4d4e9fcc2f438d39a4e854f39cc0f5f98", size = 31064409, upload-time = "2024-06-13T18:08:34.006Z" }, + { url = "https://files.pythonhosted.org/packages/ee/e1/38deed89ced4cf378c61e232265cfe933ccde56ae83c901aa68b477d14b1/llvmlite-0.43.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0a9a1a39d4bf3517f2af9d23d479b4175ead205c592ceeb8b89af48a327ea57", size = 28793149, upload-time = "2024-06-13T18:08:37.42Z" }, + { url = "https://files.pythonhosted.org/packages/2f/b2/4429433eb2dc8379e2cb582502dca074c23837f8fd009907f78a24de4c25/llvmlite-0.43.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1da416ab53e4f7f3bc8d4eeba36d801cc1894b9fbfbf2022b29b6bad34a7df2", size = 42857277, upload-time = "2024-06-13T18:08:40.822Z" }, + { url = "https://files.pythonhosted.org/packages/6b/99/5d00a7d671b1ba1751fc9f19d3b36f3300774c6eebe2bcdb5f6191763eb4/llvmlite-0.43.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977525a1e5f4059316b183fb4fd34fa858c9eade31f165427a3977c95e3ee749", size = 43871781, upload-time = "2024-06-13T18:08:46.41Z" }, + { url = "https://files.pythonhosted.org/packages/20/ab/ed5ed3688c6ba4f0b8d789da19fd8e30a9cf7fc5852effe311bc5aefe73e/llvmlite-0.43.0-cp311-cp311-win_amd64.whl", hash = "sha256:d5bd550001d26450bd90777736c69d68c487d17bf371438f975229b2b8241a91", size = 28107433, upload-time = "2024-06-13T18:08:50.834Z" }, + { url = "https://files.pythonhosted.org/packages/0b/67/9443509e5d2b6d8587bae3ede5598fa8bd586b1c7701696663ea8af15b5b/llvmlite-0.43.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f99b600aa7f65235a5a05d0b9a9f31150c390f31261f2a0ba678e26823ec38f7", size = 31064409, upload-time = "2024-06-13T18:08:54.375Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9c/24139d3712d2d352e300c39c0e00d167472c08b3bd350c3c33d72c88ff8d/llvmlite-0.43.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:35d80d61d0cda2d767f72de99450766250560399edc309da16937b93d3b676e7", size = 28793145, upload-time = "2024-06-13T18:08:57.953Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f1/4c205a48488e574ee9f6505d50e84370a978c90f08dab41a42d8f2c576b6/llvmlite-0.43.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eccce86bba940bae0d8d48ed925f21dbb813519169246e2ab292b5092aba121f", size = 42857276, upload-time = "2024-06-13T18:09:02.067Z" }, + { url = "https://files.pythonhosted.org/packages/00/5f/323c4d56e8401c50185fd0e875fcf06b71bf825a863699be1eb10aa2a9cb/llvmlite-0.43.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df6509e1507ca0760787a199d19439cc887bfd82226f5af746d6977bd9f66844", size = 43871781, upload-time = "2024-06-13T18:09:06.667Z" }, + { url = "https://files.pythonhosted.org/packages/c6/94/dea10e263655ce78d777e78d904903faae39d1fc440762be4a9dc46bed49/llvmlite-0.43.0-cp312-cp312-win_amd64.whl", hash = "sha256:7a2872ee80dcf6b5dbdc838763d26554c2a18aa833d31a2635bff16aafefb9c9", size = 28107442, upload-time = "2024-06-13T18:09:10.709Z" }, + { url = "https://files.pythonhosted.org/packages/2a/73/12925b1bbb3c2beb6d96f892ef5b4d742c34f00ddb9f4a125e9e87b22f52/llvmlite-0.43.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9cd2a7376f7b3367019b664c21f0c61766219faa3b03731113ead75107f3b66c", size = 31064410, upload-time = "2024-06-13T18:09:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/cc/61/58c70aa0808a8cba825a7d98cc65bef4801b99328fba80837bfcb5fc767f/llvmlite-0.43.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18e9953c748b105668487b7c81a3e97b046d8abf95c4ddc0cd3c94f4e4651ae8", size = 28793145, upload-time = "2024-06-13T18:09:17.531Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c6/9324eb5de2ba9d99cbed853d85ba7a318652a48e077797bec27cf40f911d/llvmlite-0.43.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74937acd22dc11b33946b67dca7680e6d103d6e90eeaaaf932603bec6fe7b03a", size = 42857276, upload-time = "2024-06-13T18:09:21.377Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d0/889e9705107db7b1ec0767b03f15d7b95b4c4f9fdf91928ab1c7e9ffacf6/llvmlite-0.43.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc9efc739cc6ed760f795806f67889923f7274276f0eb45092a1473e40d9b867", size = 43871777, upload-time = "2024-06-13T18:09:25.76Z" }, + { url = "https://files.pythonhosted.org/packages/df/41/73cc26a2634b538cfe813f618c91e7e9960b8c163f8f0c94a2b0f008b9da/llvmlite-0.43.0-cp39-cp39-win_amd64.whl", hash = "sha256:47e147cdda9037f94b399bf03bfd8a6b6b1f2f90be94a454e3386f006455a9b4", size = 28123489, upload-time = "2024-06-13T18:09:29.78Z" }, +] + +[[package]] +name = "llvmlite" +version = "0.44.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/89/6a/95a3d3610d5c75293d5dbbb2a76480d5d4eeba641557b69fe90af6c5b84e/llvmlite-0.44.0.tar.gz", hash = "sha256:07667d66a5d150abed9157ab6c0b9393c9356f229784a4385c02f99e94fc94d4", size = 171880, upload-time = "2025-01-20T11:14:41.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/75/d4863ddfd8ab5f6e70f4504cf8cc37f4e986ec6910f4ef8502bb7d3c1c71/llvmlite-0.44.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:9fbadbfba8422123bab5535b293da1cf72f9f478a65645ecd73e781f962ca614", size = 28132306, upload-time = "2025-01-20T11:12:18.634Z" }, + { url = "https://files.pythonhosted.org/packages/37/d9/6e8943e1515d2f1003e8278819ec03e4e653e2eeb71e4d00de6cfe59424e/llvmlite-0.44.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cccf8eb28f24840f2689fb1a45f9c0f7e582dd24e088dcf96e424834af11f791", size = 26201096, upload-time = "2025-01-20T11:12:24.544Z" }, + { url = "https://files.pythonhosted.org/packages/aa/46/8ffbc114def88cc698906bf5acab54ca9fdf9214fe04aed0e71731fb3688/llvmlite-0.44.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7202b678cdf904823c764ee0fe2dfe38a76981f4c1e51715b4cb5abb6cf1d9e8", size = 42361859, upload-time = "2025-01-20T11:12:31.839Z" }, + { url = "https://files.pythonhosted.org/packages/30/1c/9366b29ab050a726af13ebaae8d0dff00c3c58562261c79c635ad4f5eb71/llvmlite-0.44.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40526fb5e313d7b96bda4cbb2c85cd5374e04d80732dd36a282d72a560bb6408", size = 41184199, upload-time = "2025-01-20T11:12:40.049Z" }, + { url = "https://files.pythonhosted.org/packages/69/07/35e7c594b021ecb1938540f5bce543ddd8713cff97f71d81f021221edc1b/llvmlite-0.44.0-cp310-cp310-win_amd64.whl", hash = "sha256:41e3839150db4330e1b2716c0be3b5c4672525b4c9005e17c7597f835f351ce2", size = 30332381, upload-time = "2025-01-20T11:12:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e2/86b245397052386595ad726f9742e5223d7aea999b18c518a50e96c3aca4/llvmlite-0.44.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:eed7d5f29136bda63b6d7804c279e2b72e08c952b7c5df61f45db408e0ee52f3", size = 28132305, upload-time = "2025-01-20T11:12:53.936Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ec/506902dc6870249fbe2466d9cf66d531265d0f3a1157213c8f986250c033/llvmlite-0.44.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ace564d9fa44bb91eb6e6d8e7754977783c68e90a471ea7ce913bff30bd62427", size = 26201090, upload-time = "2025-01-20T11:12:59.847Z" }, + { url = "https://files.pythonhosted.org/packages/99/fe/d030f1849ebb1f394bb3f7adad5e729b634fb100515594aca25c354ffc62/llvmlite-0.44.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5d22c3bfc842668168a786af4205ec8e3ad29fb1bc03fd11fd48460d0df64c1", size = 42361858, upload-time = "2025-01-20T11:13:07.623Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7a/ce6174664b9077fc673d172e4c888cb0b128e707e306bc33fff8c2035f0d/llvmlite-0.44.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f01a394e9c9b7b1d4e63c327b096d10f6f0ed149ef53d38a09b3749dcf8c9610", size = 41184200, upload-time = "2025-01-20T11:13:20.058Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c6/258801143975a6d09a373f2641237992496e15567b907a4d401839d671b8/llvmlite-0.44.0-cp311-cp311-win_amd64.whl", hash = "sha256:d8489634d43c20cd0ad71330dde1d5bc7b9966937a263ff1ec1cebb90dc50955", size = 30331193, upload-time = "2025-01-20T11:13:26.976Z" }, + { url = "https://files.pythonhosted.org/packages/15/86/e3c3195b92e6e492458f16d233e58a1a812aa2bfbef9bdd0fbafcec85c60/llvmlite-0.44.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:1d671a56acf725bf1b531d5ef76b86660a5ab8ef19bb6a46064a705c6ca80aad", size = 28132297, upload-time = "2025-01-20T11:13:32.57Z" }, + { url = "https://files.pythonhosted.org/packages/d6/53/373b6b8be67b9221d12b24125fd0ec56b1078b660eeae266ec388a6ac9a0/llvmlite-0.44.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f79a728e0435493611c9f405168682bb75ffd1fbe6fc360733b850c80a026db", size = 26201105, upload-time = "2025-01-20T11:13:38.744Z" }, + { url = "https://files.pythonhosted.org/packages/cb/da/8341fd3056419441286c8e26bf436923021005ece0bff5f41906476ae514/llvmlite-0.44.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0143a5ef336da14deaa8ec26c5449ad5b6a2b564df82fcef4be040b9cacfea9", size = 42361901, upload-time = "2025-01-20T11:13:46.711Z" }, + { url = "https://files.pythonhosted.org/packages/53/ad/d79349dc07b8a395a99153d7ce8b01d6fcdc9f8231355a5df55ded649b61/llvmlite-0.44.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d752f89e31b66db6f8da06df8b39f9b91e78c5feea1bf9e8c1fba1d1c24c065d", size = 41184247, upload-time = "2025-01-20T11:13:56.159Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3b/a9a17366af80127bd09decbe2a54d8974b6d8b274b39bf47fbaedeec6307/llvmlite-0.44.0-cp312-cp312-win_amd64.whl", hash = "sha256:eae7e2d4ca8f88f89d315b48c6b741dcb925d6a1042da694aa16ab3dd4cbd3a1", size = 30332380, upload-time = "2025-01-20T11:14:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/89/24/4c0ca705a717514c2092b18476e7a12c74d34d875e05e4d742618ebbf449/llvmlite-0.44.0-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:319bddd44e5f71ae2689859b7203080716448a3cd1128fb144fe5c055219d516", size = 28132306, upload-time = "2025-01-20T11:14:09.035Z" }, + { url = "https://files.pythonhosted.org/packages/01/cf/1dd5a60ba6aee7122ab9243fd614abcf22f36b0437cbbe1ccf1e3391461c/llvmlite-0.44.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c58867118bad04a0bb22a2e0068c693719658105e40009ffe95c7000fcde88e", size = 26201090, upload-time = "2025-01-20T11:14:15.401Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1b/656f5a357de7135a3777bd735cc7c9b8f23b4d37465505bd0eaf4be9befe/llvmlite-0.44.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46224058b13c96af1365290bdfebe9a6264ae62fb79b2b55693deed11657a8bf", size = 42361904, upload-time = "2025-01-20T11:14:22.949Z" }, + { url = "https://files.pythonhosted.org/packages/d8/e1/12c5f20cb9168fb3464a34310411d5ad86e4163c8ff2d14a2b57e5cc6bac/llvmlite-0.44.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0097052c32bf721a4efc03bd109d335dfa57d9bffb3d4c24cc680711b8b4fc", size = 41184245, upload-time = "2025-01-20T11:14:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/d0/81/e66fc86539293282fd9cb7c9417438e897f369e79ffb62e1ae5e5154d4dd/llvmlite-0.44.0-cp313-cp313-win_amd64.whl", hash = "sha256:2fb7c4f2fb86cbae6dca3db9ab203eeea0e22d73b99bc2341cdf9de93612e930", size = 30331193, upload-time = "2025-01-20T11:14:38.578Z" }, +] + +[[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 = "matplotlib" +version = "3.9.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "contourpy", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "cycler", marker = "python_full_version < '3.10'" }, + { name = "fonttools", marker = "python_full_version < '3.10'" }, + { name = "importlib-resources", marker = "python_full_version < '3.10'" }, + { name = "kiwisolver", version = "1.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pillow", marker = "python_full_version < '3.10'" }, + { name = "pyparsing", marker = "python_full_version < '3.10'" }, + { name = "python-dateutil", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/17/1747b4154034befd0ed33b52538f5eb7752d05bb51c5e2a31470c3bc7d52/matplotlib-3.9.4.tar.gz", hash = "sha256:1e00e8be7393cbdc6fedfa8a6fba02cf3e83814b285db1c60b906a023ba41bc3", size = 36106529, upload-time = "2024-12-13T05:56:34.184Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/94/27d2e2c30d54b56c7b764acc1874a909e34d1965a427fc7092bb6a588b63/matplotlib-3.9.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c5fdd7abfb706dfa8d307af64a87f1a862879ec3cd8d0ec8637458f0885b9c50", size = 7885089, upload-time = "2024-12-13T05:54:24.224Z" }, + { url = "https://files.pythonhosted.org/packages/c6/25/828273307e40a68eb8e9df832b6b2aaad075864fdc1de4b1b81e40b09e48/matplotlib-3.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d89bc4e85e40a71d1477780366c27fb7c6494d293e1617788986f74e2a03d7ff", size = 7770600, upload-time = "2024-12-13T05:54:27.214Z" }, + { url = "https://files.pythonhosted.org/packages/f2/65/f841a422ec994da5123368d76b126acf4fc02ea7459b6e37c4891b555b83/matplotlib-3.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddf9f3c26aae695c5daafbf6b94e4c1a30d6cd617ba594bbbded3b33a1fcfa26", size = 8200138, upload-time = "2024-12-13T05:54:29.497Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/272aca07a38804d93b6050813de41ca7ab0e29ba7a9dd098e12037c919a9/matplotlib-3.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18ebcf248030173b59a868fda1fe42397253f6698995b55e81e1f57431d85e50", size = 8312711, upload-time = "2024-12-13T05:54:34.396Z" }, + { url = "https://files.pythonhosted.org/packages/98/37/f13e23b233c526b7e27ad61be0a771894a079e0f7494a10d8d81557e0e9a/matplotlib-3.9.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:974896ec43c672ec23f3f8c648981e8bc880ee163146e0312a9b8def2fac66f5", size = 9090622, upload-time = "2024-12-13T05:54:36.808Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8c/b1f5bd2bd70e60f93b1b54c4d5ba7a992312021d0ddddf572f9a1a6d9348/matplotlib-3.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:4598c394ae9711cec135639374e70871fa36b56afae17bdf032a345be552a88d", size = 7828211, upload-time = "2024-12-13T05:54:40.596Z" }, + { url = "https://files.pythonhosted.org/packages/74/4b/65be7959a8fa118a3929b49a842de5b78bb55475236fcf64f3e308ff74a0/matplotlib-3.9.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d4dd29641d9fb8bc4492420c5480398dd40a09afd73aebe4eb9d0071a05fbe0c", size = 7894430, upload-time = "2024-12-13T05:54:44.049Z" }, + { url = "https://files.pythonhosted.org/packages/e9/18/80f70d91896e0a517b4a051c3fd540daa131630fd75e02e250365353b253/matplotlib-3.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30e5b22e8bcfb95442bf7d48b0d7f3bdf4a450cbf68986ea45fca3d11ae9d099", size = 7780045, upload-time = "2024-12-13T05:54:46.414Z" }, + { url = "https://files.pythonhosted.org/packages/a2/73/ccb381026e3238c5c25c3609ba4157b2d1a617ec98d65a8b4ee4e1e74d02/matplotlib-3.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bb0030d1d447fd56dcc23b4c64a26e44e898f0416276cac1ebc25522e0ac249", size = 8209906, upload-time = "2024-12-13T05:54:49.459Z" }, + { url = "https://files.pythonhosted.org/packages/ab/33/1648da77b74741c89f5ea95cbf42a291b4b364f2660b316318811404ed97/matplotlib-3.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aca90ed222ac3565d2752b83dbb27627480d27662671e4d39da72e97f657a423", size = 8322873, upload-time = "2024-12-13T05:54:53.066Z" }, + { url = "https://files.pythonhosted.org/packages/57/d3/8447ba78bc6593c9044c372d1609f8ea10fb1e071e7a9e0747bea74fc16c/matplotlib-3.9.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a181b2aa2906c608fcae72f977a4a2d76e385578939891b91c2550c39ecf361e", size = 9099566, upload-time = "2024-12-13T05:54:55.522Z" }, + { url = "https://files.pythonhosted.org/packages/23/e1/4f0e237bf349c02ff9d1b6e7109f1a17f745263809b9714a8576dc17752b/matplotlib-3.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:1f6882828231eca17f501c4dcd98a05abb3f03d157fbc0769c6911fe08b6cfd3", size = 7838065, upload-time = "2024-12-13T05:54:58.337Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2b/c918bf6c19d6445d1cefe3d2e42cb740fb997e14ab19d4daeb6a7ab8a157/matplotlib-3.9.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dfc48d67e6661378a21c2983200a654b72b5c5cdbd5d2cf6e5e1ece860f0cc70", size = 7891131, upload-time = "2024-12-13T05:55:02.837Z" }, + { url = "https://files.pythonhosted.org/packages/c1/e5/b4e8fc601ca302afeeabf45f30e706a445c7979a180e3a978b78b2b681a4/matplotlib-3.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47aef0fab8332d02d68e786eba8113ffd6f862182ea2999379dec9e237b7e483", size = 7776365, upload-time = "2024-12-13T05:55:05.158Z" }, + { url = "https://files.pythonhosted.org/packages/99/06/b991886c506506476e5d83625c5970c656a491b9f80161458fed94597808/matplotlib-3.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fba1f52c6b7dc764097f52fd9ab627b90db452c9feb653a59945de16752e965f", size = 8200707, upload-time = "2024-12-13T05:55:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e2/556b627498cb27e61026f2d1ba86a78ad1b836fef0996bef5440e8bc9559/matplotlib-3.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:173ac3748acaac21afcc3fa1633924609ba1b87749006bc25051c52c422a5d00", size = 8313761, upload-time = "2024-12-13T05:55:12.95Z" }, + { url = "https://files.pythonhosted.org/packages/58/ff/165af33ec766ff818306ea88e91f9f60d2a6ed543be1eb122a98acbf3b0d/matplotlib-3.9.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320edea0cadc07007765e33f878b13b3738ffa9745c5f707705692df70ffe0e0", size = 9095284, upload-time = "2024-12-13T05:55:16.199Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8b/3d0c7a002db3b1ed702731c2a9a06d78d035f1f2fb0fb936a8e43cc1e9f4/matplotlib-3.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a4a4cfc82330b27042a7169533da7991e8789d180dd5b3daeaee57d75cd5a03b", size = 7841160, upload-time = "2024-12-13T05:55:19.991Z" }, + { url = "https://files.pythonhosted.org/packages/49/b1/999f89a7556d101b23a2f0b54f1b6e140d73f56804da1398f2f0bc0924bc/matplotlib-3.9.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37eeffeeca3c940985b80f5b9a7b95ea35671e0e7405001f249848d2b62351b6", size = 7891499, upload-time = "2024-12-13T05:55:22.142Z" }, + { url = "https://files.pythonhosted.org/packages/87/7b/06a32b13a684977653396a1bfcd34d4e7539c5d55c8cbfaa8ae04d47e4a9/matplotlib-3.9.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3e7465ac859ee4abcb0d836137cd8414e7bb7ad330d905abced457217d4f0f45", size = 7776802, upload-time = "2024-12-13T05:55:25.947Z" }, + { url = "https://files.pythonhosted.org/packages/65/87/ac498451aff739e515891bbb92e566f3c7ef31891aaa878402a71f9b0910/matplotlib-3.9.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4c12302c34afa0cf061bea23b331e747e5e554b0fa595c96e01c7b75bc3b858", size = 8200802, upload-time = "2024-12-13T05:55:28.461Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6b/9eb761c00e1cb838f6c92e5f25dcda3f56a87a52f6cb8fdfa561e6cf6a13/matplotlib-3.9.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b8c97917f21b75e72108b97707ba3d48f171541a74aa2a56df7a40626bafc64", size = 8313880, upload-time = "2024-12-13T05:55:30.965Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a2/c8eaa600e2085eec7e38cbbcc58a30fc78f8224939d31d3152bdafc01fd1/matplotlib-3.9.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0229803bd7e19271b03cb09f27db76c918c467aa4ce2ae168171bc67c3f508df", size = 9094637, upload-time = "2024-12-13T05:55:33.701Z" }, + { url = "https://files.pythonhosted.org/packages/71/1f/c6e1daea55b7bfeb3d84c6cb1abc449f6a02b181e7e2a5e4db34c3afb793/matplotlib-3.9.4-cp313-cp313-win_amd64.whl", hash = "sha256:7c0d8ef442ebf56ff5e206f8083d08252ee738e04f3dc88ea882853a05488799", size = 7841311, upload-time = "2024-12-13T05:55:36.737Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3a/2757d3f7d388b14dd48f5a83bea65b6d69f000e86b8f28f74d86e0d375bd/matplotlib-3.9.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a04c3b00066a688834356d196136349cb32f5e1003c55ac419e91585168b88fb", size = 7919989, upload-time = "2024-12-13T05:55:39.024Z" }, + { url = "https://files.pythonhosted.org/packages/24/28/f5077c79a4f521589a37fe1062d6a6ea3534e068213f7357e7cfffc2e17a/matplotlib-3.9.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04c519587f6c210626741a1e9a68eefc05966ede24205db8982841826af5871a", size = 7809417, upload-time = "2024-12-13T05:55:42.412Z" }, + { url = "https://files.pythonhosted.org/packages/36/c8/c523fd2963156692916a8eb7d4069084cf729359f7955cf09075deddfeaf/matplotlib-3.9.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:308afbf1a228b8b525fcd5cec17f246bbbb63b175a3ef6eb7b4d33287ca0cf0c", size = 8226258, upload-time = "2024-12-13T05:55:47.259Z" }, + { url = "https://files.pythonhosted.org/packages/f6/88/499bf4b8fa9349b6f5c0cf4cead0ebe5da9d67769129f1b5651e5ac51fbc/matplotlib-3.9.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddb3b02246ddcffd3ce98e88fed5b238bc5faff10dbbaa42090ea13241d15764", size = 8335849, upload-time = "2024-12-13T05:55:49.763Z" }, + { url = "https://files.pythonhosted.org/packages/b8/9f/20a4156b9726188646a030774ee337d5ff695a965be45ce4dbcb9312c170/matplotlib-3.9.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8a75287e9cb9eee48cb79ec1d806f75b29c0fde978cb7223a1f4c5848d696041", size = 9102152, upload-time = "2024-12-13T05:55:51.997Z" }, + { url = "https://files.pythonhosted.org/packages/10/11/237f9c3a4e8d810b1759b67ff2da7c32c04f9c80aa475e7beb36ed43a8fb/matplotlib-3.9.4-cp313-cp313t-win_amd64.whl", hash = "sha256:488deb7af140f0ba86da003e66e10d55ff915e152c78b4b66d231638400b1965", size = 7896987, upload-time = "2024-12-13T05:55:55.941Z" }, + { url = "https://files.pythonhosted.org/packages/56/eb/501b465c9fef28f158e414ea3a417913dc2ac748564c7ed41535f23445b4/matplotlib-3.9.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3c3724d89a387ddf78ff88d2a30ca78ac2b4c89cf37f2db4bd453c34799e933c", size = 7885919, upload-time = "2024-12-13T05:55:59.66Z" }, + { url = "https://files.pythonhosted.org/packages/da/36/236fbd868b6c91309a5206bd90c3f881f4f44b2d997cd1d6239ef652f878/matplotlib-3.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d5f0a8430ffe23d7e32cfd86445864ccad141797f7d25b7c41759a5b5d17cfd7", size = 7771486, upload-time = "2024-12-13T05:56:04.264Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4b/105caf2d54d5ed11d9f4335398f5103001a03515f2126c936a752ccf1461/matplotlib-3.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bb0141a21aef3b64b633dc4d16cbd5fc538b727e4958be82a0e1c92a234160e", size = 8201838, upload-time = "2024-12-13T05:56:06.792Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a7/bb01188fb4013d34d274caf44a2f8091255b0497438e8b6c0a7c1710c692/matplotlib-3.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57aa235109e9eed52e2c2949db17da185383fa71083c00c6c143a60e07e0888c", size = 8314492, upload-time = "2024-12-13T05:56:09.964Z" }, + { url = "https://files.pythonhosted.org/packages/33/19/02e1a37f7141fc605b193e927d0a9cdf9dc124a20b9e68793f4ffea19695/matplotlib-3.9.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b18c600061477ccfdd1e6fd050c33d8be82431700f3452b297a56d9ed7037abb", size = 9092500, upload-time = "2024-12-13T05:56:13.55Z" }, + { url = "https://files.pythonhosted.org/packages/57/68/c2feb4667adbf882ffa4b3e0ac9967f848980d9f8b5bebd86644aa67ce6a/matplotlib-3.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:ef5f2d1b67d2d2145ff75e10f8c008bfbf71d45137c4b648c87193e7dd053eac", size = 7822962, upload-time = "2024-12-13T05:56:16.358Z" }, + { url = "https://files.pythonhosted.org/packages/0c/22/2ef6a364cd3f565442b0b055e0599744f1e4314ec7326cdaaa48a4d864d7/matplotlib-3.9.4-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:44e0ed786d769d85bc787b0606a53f2d8d2d1d3c8a2608237365e9121c1a338c", size = 7877995, upload-time = "2024-12-13T05:56:18.805Z" }, + { url = "https://files.pythonhosted.org/packages/87/b8/2737456e566e9f4d94ae76b8aa0d953d9acb847714f9a7ad80184474f5be/matplotlib-3.9.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:09debb9ce941eb23ecdbe7eab972b1c3e0276dcf01688073faff7b0f61d6c6ca", size = 7769300, upload-time = "2024-12-13T05:56:21.315Z" }, + { url = "https://files.pythonhosted.org/packages/b2/1f/e709c6ec7b5321e6568769baa288c7178e60a93a9da9e682b39450da0e29/matplotlib-3.9.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcc53cf157a657bfd03afab14774d54ba73aa84d42cfe2480c91bd94873952db", size = 8313423, upload-time = "2024-12-13T05:56:26.719Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b6/5a1f868782cd13f053a679984e222007ecff654a9bfbac6b27a65f4eeb05/matplotlib-3.9.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ad45da51be7ad02387801fd154ef74d942f49fe3fcd26a64c94842ba7ec0d865", size = 7854624, upload-time = "2024-12-13T05:56:29.359Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "cycler", marker = "python_full_version >= '3.10'" }, + { name = "fonttools", marker = "python_full_version >= '3.10'" }, + { name = "kiwisolver", version = "1.4.8", 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 = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "pillow", marker = "python_full_version >= '3.10'" }, + { name = "pyparsing", marker = "python_full_version >= '3.10'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/91/d49359a21893183ed2a5b6c76bec40e0b1dcbf8ca148f864d134897cfc75/matplotlib-3.10.3.tar.gz", hash = "sha256:2f82d2c5bb7ae93aaaa4cd42aca65d76ce6376f83304fa3a630b569aca274df0", size = 34799811, upload-time = "2025-05-08T19:10:54.39Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/ea/2bba25d289d389c7451f331ecd593944b3705f06ddf593fa7be75037d308/matplotlib-3.10.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:213fadd6348d106ca7db99e113f1bea1e65e383c3ba76e8556ba4a3054b65ae7", size = 8167862, upload-time = "2025-05-08T19:09:39.563Z" }, + { url = "https://files.pythonhosted.org/packages/41/81/cc70b5138c926604e8c9ed810ed4c79e8116ba72e02230852f5c12c87ba2/matplotlib-3.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3bec61cb8221f0ca6313889308326e7bb303d0d302c5cc9e523b2f2e6c73deb", size = 8042149, upload-time = "2025-05-08T19:09:42.413Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/0ff45b6bfa42bb16de597e6058edf2361c298ad5ef93b327728145161bbf/matplotlib-3.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c21ae75651c0231b3ba014b6d5e08fb969c40cdb5a011e33e99ed0c9ea86ecb", size = 8453719, upload-time = "2025-05-08T19:09:44.901Z" }, + { url = "https://files.pythonhosted.org/packages/85/c7/1866e972fed6d71ef136efbc980d4d1854ab7ef1ea8152bbd995ca231c81/matplotlib-3.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a49e39755580b08e30e3620efc659330eac5d6534ab7eae50fa5e31f53ee4e30", size = 8590801, upload-time = "2025-05-08T19:09:47.404Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b9/748f6626d534ab7e255bdc39dc22634d337cf3ce200f261b5d65742044a1/matplotlib-3.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cf4636203e1190871d3a73664dea03d26fb019b66692cbfd642faafdad6208e8", size = 9402111, upload-time = "2025-05-08T19:09:49.474Z" }, + { url = "https://files.pythonhosted.org/packages/1f/78/8bf07bd8fb67ea5665a6af188e70b57fcb2ab67057daa06b85a08e59160a/matplotlib-3.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:fd5641a9bb9d55f4dd2afe897a53b537c834b9012684c8444cc105895c8c16fd", size = 8057213, upload-time = "2025-05-08T19:09:51.489Z" }, + { url = "https://files.pythonhosted.org/packages/f5/bd/af9f655456f60fe1d575f54fb14704ee299b16e999704817a7645dfce6b0/matplotlib-3.10.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0ef061f74cd488586f552d0c336b2f078d43bc00dc473d2c3e7bfee2272f3fa8", size = 8178873, upload-time = "2025-05-08T19:09:53.857Z" }, + { url = "https://files.pythonhosted.org/packages/c2/86/e1c86690610661cd716eda5f9d0b35eaf606ae6c9b6736687cfc8f2d0cd8/matplotlib-3.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96985d14dc5f4a736bbea4b9de9afaa735f8a0fc2ca75be2fa9e96b2097369d", size = 8052205, upload-time = "2025-05-08T19:09:55.684Z" }, + { url = "https://files.pythonhosted.org/packages/54/51/a9f8e49af3883dacddb2da1af5fca1f7468677f1188936452dd9aaaeb9ed/matplotlib-3.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c5f0283da91e9522bdba4d6583ed9d5521566f63729ffb68334f86d0bb98049", size = 8465823, upload-time = "2025-05-08T19:09:57.442Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e3/c82963a3b86d6e6d5874cbeaa390166458a7f1961bab9feb14d3d1a10f02/matplotlib-3.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdfa07c0ec58035242bc8b2c8aae37037c9a886370eef6850703d7583e19964b", size = 8606464, upload-time = "2025-05-08T19:09:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/0e/34/24da1027e7fcdd9e82da3194c470143c551852757a4b473a09a012f5b945/matplotlib-3.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c0b9849a17bce080a16ebcb80a7b714b5677d0ec32161a2cc0a8e5a6030ae220", size = 9413103, upload-time = "2025-05-08T19:10:03.208Z" }, + { url = "https://files.pythonhosted.org/packages/a6/da/948a017c3ea13fd4a97afad5fdebe2f5bbc4d28c0654510ce6fd6b06b7bd/matplotlib-3.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:eef6ed6c03717083bc6d69c2d7ee8624205c29a8e6ea5a31cd3492ecdbaee1e1", size = 8065492, upload-time = "2025-05-08T19:10:05.271Z" }, + { url = "https://files.pythonhosted.org/packages/eb/43/6b80eb47d1071f234ef0c96ca370c2ca621f91c12045f1401b5c9b28a639/matplotlib-3.10.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ab1affc11d1f495ab9e6362b8174a25afc19c081ba5b0775ef00533a4236eea", size = 8179689, upload-time = "2025-05-08T19:10:07.602Z" }, + { url = "https://files.pythonhosted.org/packages/0f/70/d61a591958325c357204870b5e7b164f93f2a8cca1dc6ce940f563909a13/matplotlib-3.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2a818d8bdcafa7ed2eed74487fdb071c09c1ae24152d403952adad11fa3c65b4", size = 8050466, upload-time = "2025-05-08T19:10:09.383Z" }, + { url = "https://files.pythonhosted.org/packages/e7/75/70c9d2306203148cc7902a961240c5927dd8728afedf35e6a77e105a2985/matplotlib-3.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748ebc3470c253e770b17d8b0557f0aa85cf8c63fd52f1a61af5b27ec0b7ffee", size = 8456252, upload-time = "2025-05-08T19:10:11.958Z" }, + { url = "https://files.pythonhosted.org/packages/c4/91/ba0ae1ff4b3f30972ad01cd4a8029e70a0ec3b8ea5be04764b128b66f763/matplotlib-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed70453fd99733293ace1aec568255bc51c6361cb0da94fa5ebf0649fdb2150a", size = 8601321, upload-time = "2025-05-08T19:10:14.47Z" }, + { url = "https://files.pythonhosted.org/packages/d2/88/d636041eb54a84b889e11872d91f7cbf036b3b0e194a70fa064eb8b04f7a/matplotlib-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbed9917b44070e55640bd13419de83b4c918e52d97561544814ba463811cbc7", size = 9406972, upload-time = "2025-05-08T19:10:16.569Z" }, + { url = "https://files.pythonhosted.org/packages/b1/79/0d1c165eac44405a86478082e225fce87874f7198300bbebc55faaf6d28d/matplotlib-3.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:cf37d8c6ef1a48829443e8ba5227b44236d7fcaf7647caa3178a4ff9f7a5be05", size = 8067954, upload-time = "2025-05-08T19:10:18.663Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c1/23cfb566a74c696a3b338d8955c549900d18fe2b898b6e94d682ca21e7c2/matplotlib-3.10.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9f2efccc8dcf2b86fc4ee849eea5dcaecedd0773b30f47980dc0cbeabf26ec84", size = 8180318, upload-time = "2025-05-08T19:10:20.426Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0c/02f1c3b66b30da9ee343c343acbb6251bef5b01d34fad732446eaadcd108/matplotlib-3.10.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ddbba06a6c126e3301c3d272a99dcbe7f6c24c14024e80307ff03791a5f294e", size = 8051132, upload-time = "2025-05-08T19:10:22.569Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ab/8db1a5ac9b3a7352fb914133001dae889f9fcecb3146541be46bed41339c/matplotlib-3.10.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748302b33ae9326995b238f606e9ed840bf5886ebafcb233775d946aa8107a15", size = 8457633, upload-time = "2025-05-08T19:10:24.749Z" }, + { url = "https://files.pythonhosted.org/packages/f5/64/41c4367bcaecbc03ef0d2a3ecee58a7065d0a36ae1aa817fe573a2da66d4/matplotlib-3.10.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a80fcccbef63302c0efd78042ea3c2436104c5b1a4d3ae20f864593696364ac7", size = 8601031, upload-time = "2025-05-08T19:10:27.03Z" }, + { url = "https://files.pythonhosted.org/packages/12/6f/6cc79e9e5ab89d13ed64da28898e40fe5b105a9ab9c98f83abd24e46d7d7/matplotlib-3.10.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55e46cbfe1f8586adb34f7587c3e4f7dedc59d5226719faf6cb54fc24f2fd52d", size = 9406988, upload-time = "2025-05-08T19:10:29.056Z" }, + { url = "https://files.pythonhosted.org/packages/b1/0f/eed564407bd4d935ffabf561ed31099ed609e19287409a27b6d336848653/matplotlib-3.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:151d89cb8d33cb23345cd12490c76fd5d18a56581a16d950b48c6ff19bb2ab93", size = 8068034, upload-time = "2025-05-08T19:10:31.221Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e5/2f14791ff69b12b09e9975e1d116d9578ac684460860ce542c2588cb7a1c/matplotlib-3.10.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c26dd9834e74d164d06433dc7be5d75a1e9890b926b3e57e74fa446e1a62c3e2", size = 8218223, upload-time = "2025-05-08T19:10:33.114Z" }, + { url = "https://files.pythonhosted.org/packages/5c/08/30a94afd828b6e02d0a52cae4a29d6e9ccfcf4c8b56cc28b021d3588873e/matplotlib-3.10.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:24853dad5b8c84c8c2390fc31ce4858b6df504156893292ce8092d190ef8151d", size = 8094985, upload-time = "2025-05-08T19:10:35.337Z" }, + { url = "https://files.pythonhosted.org/packages/89/44/f3bc6b53066c889d7a1a3ea8094c13af6a667c5ca6220ec60ecceec2dabe/matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68f7878214d369d7d4215e2a9075fef743be38fa401d32e6020bab2dfabaa566", size = 8483109, upload-time = "2025-05-08T19:10:37.611Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c7/473bc559beec08ebee9f86ca77a844b65747e1a6c2691e8c92e40b9f42a8/matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6929fc618cb6db9cb75086f73b3219bbb25920cb24cee2ea7a12b04971a4158", size = 8618082, upload-time = "2025-05-08T19:10:39.892Z" }, + { url = "https://files.pythonhosted.org/packages/d8/e9/6ce8edd264c8819e37bbed8172e0ccdc7107fe86999b76ab5752276357a4/matplotlib-3.10.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c7818292a5cc372a2dc4c795e5c356942eb8350b98ef913f7fda51fe175ac5d", size = 9413699, upload-time = "2025-05-08T19:10:42.376Z" }, + { url = "https://files.pythonhosted.org/packages/1b/92/9a45c91089c3cf690b5badd4be81e392ff086ccca8a1d4e3a08463d8a966/matplotlib-3.10.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4f23ffe95c5667ef8a2b56eea9b53db7f43910fa4a2d5472ae0f72b64deab4d5", size = 8139044, upload-time = "2025-05-08T19:10:44.551Z" }, + { url = "https://files.pythonhosted.org/packages/3d/d1/f54d43e95384b312ffa4a74a4326c722f3b8187aaaa12e9a84cdf3037131/matplotlib-3.10.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:86ab63d66bbc83fdb6733471d3bff40897c1e9921cba112accd748eee4bce5e4", size = 8162896, upload-time = "2025-05-08T19:10:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/24/a4/fbfc00c2346177c95b353dcf9b5a004106abe8730a62cb6f27e79df0a698/matplotlib-3.10.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a48f9c08bf7444b5d2391a83e75edb464ccda3c380384b36532a0962593a1751", size = 8039702, upload-time = "2025-05-08T19:10:49.634Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b9/59e120d24a2ec5fc2d30646adb2efb4621aab3c6d83d66fb2a7a182db032/matplotlib-3.10.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb73d8aa75a237457988f9765e4dfe1c0d2453c5ca4eabc897d4309672c8e014", size = 8594298, upload-time = "2025-05-08T19:10:51.738Z" }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159, upload-time = "2024-04-15T13:44:44.803Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899, upload-time = "2024-04-15T13:44:43.265Z" }, +] + +[[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 = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + +[[package]] +name = "numba" +version = "0.60.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "llvmlite", version = "0.43.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/93/2849300a9184775ba274aba6f82f303343669b0592b7bb0849ea713dabb0/numba-0.60.0.tar.gz", hash = "sha256:5df6158e5584eece5fc83294b949fd30b9f1125df7708862205217e068aabf16", size = 2702171, upload-time = "2024-06-13T18:11:19.869Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/cf/baa13a7e3556d73d9e38021e6d6aa4aeb30d8b94545aa8b70d0f24a1ccc4/numba-0.60.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d761de835cd38fb400d2c26bb103a2726f548dc30368853121d66201672e651", size = 2647627, upload-time = "2024-06-13T18:10:29.857Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ba/4b57fa498564457c3cc9fc9e570a6b08e6086c74220f24baaf04e54b995f/numba-0.60.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:159e618ef213fba758837f9837fb402bbe65326e60ba0633dbe6c7f274d42c1b", size = 2650322, upload-time = "2024-06-13T18:10:32.849Z" }, + { url = "https://files.pythonhosted.org/packages/28/98/7ea97ee75870a54f938a8c70f7e0be4495ba5349c5f9db09d467c4a5d5b7/numba-0.60.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1527dc578b95c7c4ff248792ec33d097ba6bef9eda466c948b68dfc995c25781", size = 3407390, upload-time = "2024-06-13T18:10:34.741Z" }, + { url = "https://files.pythonhosted.org/packages/79/58/cb4ac5b8f7ec64200460aef1fed88258fb872ceef504ab1f989d2ff0f684/numba-0.60.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe0b28abb8d70f8160798f4de9d486143200f34458d34c4a214114e445d7124e", size = 3699694, upload-time = "2024-06-13T18:10:37.295Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b0/c61a93ca947d12233ff45de506ddbf52af3f752066a0b8be4d27426e16da/numba-0.60.0-cp310-cp310-win_amd64.whl", hash = "sha256:19407ced081d7e2e4b8d8c36aa57b7452e0283871c296e12d798852bc7d7f198", size = 2687030, upload-time = "2024-06-13T18:10:39.47Z" }, + { url = "https://files.pythonhosted.org/packages/98/ad/df18d492a8f00d29a30db307904b9b296e37507034eedb523876f3a2e13e/numba-0.60.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a17b70fc9e380ee29c42717e8cc0bfaa5556c416d94f9aa96ba13acb41bdece8", size = 2647254, upload-time = "2024-06-13T18:10:41.69Z" }, + { url = "https://files.pythonhosted.org/packages/9a/51/a4dc2c01ce7a850b8e56ff6d5381d047a5daea83d12bad08aa071d34b2ee/numba-0.60.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fb02b344a2a80efa6f677aa5c40cd5dd452e1b35f8d1c2af0dfd9ada9978e4b", size = 2649970, upload-time = "2024-06-13T18:10:44.682Z" }, + { url = "https://files.pythonhosted.org/packages/f9/4c/8889ac94c0b33dca80bed11564b8c6d9ea14d7f094e674c58e5c5b05859b/numba-0.60.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f4fde652ea604ea3c86508a3fb31556a6157b2c76c8b51b1d45eb40c8598703", size = 3412492, upload-time = "2024-06-13T18:10:47.1Z" }, + { url = "https://files.pythonhosted.org/packages/57/03/2b4245b05b71c0cee667e6a0b51606dfa7f4157c9093d71c6b208385a611/numba-0.60.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4142d7ac0210cc86432b818338a2bc368dc773a2f5cf1e32ff7c5b378bd63ee8", size = 3705018, upload-time = "2024-06-13T18:10:49.539Z" }, + { url = "https://files.pythonhosted.org/packages/79/89/2d924ca60dbf949f18a6fec223a2445f5f428d9a5f97a6b29c2122319015/numba-0.60.0-cp311-cp311-win_amd64.whl", hash = "sha256:cac02c041e9b5bc8cf8f2034ff6f0dbafccd1ae9590dc146b3a02a45e53af4e2", size = 2686920, upload-time = "2024-06-13T18:10:51.937Z" }, + { url = "https://files.pythonhosted.org/packages/eb/5c/b5ec752c475e78a6c3676b67c514220dbde2725896bbb0b6ec6ea54b2738/numba-0.60.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d7da4098db31182fc5ffe4bc42c6f24cd7d1cb8a14b59fd755bfee32e34b8404", size = 2647866, upload-time = "2024-06-13T18:10:54.453Z" }, + { url = "https://files.pythonhosted.org/packages/65/42/39559664b2e7c15689a638c2a38b3b74c6e69a04e2b3019b9f7742479188/numba-0.60.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38d6ea4c1f56417076ecf8fc327c831ae793282e0ff51080c5094cb726507b1c", size = 2650208, upload-time = "2024-06-13T18:10:56.779Z" }, + { url = "https://files.pythonhosted.org/packages/67/88/c4459ccc05674ef02119abf2888ccd3e2fed12a323f52255f4982fc95876/numba-0.60.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:62908d29fb6a3229c242e981ca27e32a6e606cc253fc9e8faeb0e48760de241e", size = 3466946, upload-time = "2024-06-13T18:10:58.961Z" }, + { url = "https://files.pythonhosted.org/packages/8b/41/ac11cf33524def12aa5bd698226ae196a1185831c05ed29dc0c56eaa308b/numba-0.60.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0ebaa91538e996f708f1ab30ef4d3ddc344b64b5227b67a57aa74f401bb68b9d", size = 3761463, upload-time = "2024-06-13T18:11:01.657Z" }, + { url = "https://files.pythonhosted.org/packages/ca/bd/0fe29fcd1b6a8de479a4ed25c6e56470e467e3611c079d55869ceef2b6d1/numba-0.60.0-cp312-cp312-win_amd64.whl", hash = "sha256:f75262e8fe7fa96db1dca93d53a194a38c46da28b112b8a4aca168f0df860347", size = 2707588, upload-time = "2024-06-13T18:11:04.261Z" }, + { url = "https://files.pythonhosted.org/packages/68/1a/87c53f836cdf557083248c3f47212271f220280ff766538795e77c8c6bbf/numba-0.60.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:01ef4cd7d83abe087d644eaa3d95831b777aa21d441a23703d649e06b8e06b74", size = 2647186, upload-time = "2024-06-13T18:11:06.753Z" }, + { url = "https://files.pythonhosted.org/packages/28/14/a5baa1f2edea7b49afa4dc1bb1b126645198cf1075186853b5b497be826e/numba-0.60.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:819a3dfd4630d95fd574036f99e47212a1af41cbcb019bf8afac63ff56834449", size = 2650038, upload-time = "2024-06-13T18:11:10.869Z" }, + { url = "https://files.pythonhosted.org/packages/3b/bd/f1985719ff34e37e07bb18f9d3acd17e5a21da255f550c8eae031e2ddf5f/numba-0.60.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0b983bd6ad82fe868493012487f34eae8bf7dd94654951404114f23c3466d34b", size = 3403010, upload-time = "2024-06-13T18:11:13.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/9b/cd73d3f6617ddc8398a63ef97d8dc9139a9879b9ca8a7ca4b8789056ea46/numba-0.60.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c151748cd269ddeab66334bd754817ffc0cabd9433acb0f551697e5151917d25", size = 3695086, upload-time = "2024-06-13T18:11:15.497Z" }, + { url = "https://files.pythonhosted.org/packages/01/01/8b7b670c77c5ea0e47e283d82332969bf672ab6410d0b2610cac5b7a3ded/numba-0.60.0-cp39-cp39-win_amd64.whl", hash = "sha256:3031547a015710140e8c87226b4cfe927cac199835e5bf7d4fe5cb64e814e3ab", size = 2686978, upload-time = "2024-06-13T18:11:17.765Z" }, +] + +[[package]] +name = "numba" +version = "0.61.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "llvmlite", version = "0.44.0", 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'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/a0/e21f57604304aa03ebb8e098429222722ad99176a4f979d34af1d1ee80da/numba-0.61.2.tar.gz", hash = "sha256:8750ee147940a6637b80ecf7f95062185ad8726c8c28a2295b8ec1160a196f7d", size = 2820615, upload-time = "2025-04-09T02:58:07.659Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/ca/f470be59552ccbf9531d2d383b67ae0b9b524d435fb4a0d229fef135116e/numba-0.61.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:cf9f9fc00d6eca0c23fc840817ce9f439b9f03c8f03d6246c0e7f0cb15b7162a", size = 2775663, upload-time = "2025-04-09T02:57:34.143Z" }, + { url = "https://files.pythonhosted.org/packages/f5/13/3bdf52609c80d460a3b4acfb9fdb3817e392875c0d6270cf3fd9546f138b/numba-0.61.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ea0247617edcb5dd61f6106a56255baab031acc4257bddaeddb3a1003b4ca3fd", size = 2778344, upload-time = "2025-04-09T02:57:36.609Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7d/bfb2805bcfbd479f04f835241ecf28519f6e3609912e3a985aed45e21370/numba-0.61.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae8c7a522c26215d5f62ebec436e3d341f7f590079245a2f1008dfd498cc1642", size = 3824054, upload-time = "2025-04-09T02:57:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/e3/27/797b2004745c92955470c73c82f0e300cf033c791f45bdecb4b33b12bdea/numba-0.61.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd1e74609855aa43661edffca37346e4e8462f6903889917e9f41db40907daa2", size = 3518531, upload-time = "2025-04-09T02:57:39.709Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c6/c2fb11e50482cb310afae87a997707f6c7d8a48967b9696271347441f650/numba-0.61.2-cp310-cp310-win_amd64.whl", hash = "sha256:ae45830b129c6137294093b269ef0a22998ccc27bf7cf096ab8dcf7bca8946f9", size = 2831612, upload-time = "2025-04-09T02:57:41.559Z" }, + { url = "https://files.pythonhosted.org/packages/3f/97/c99d1056aed767503c228f7099dc11c402906b42a4757fec2819329abb98/numba-0.61.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:efd3db391df53aaa5cfbee189b6c910a5b471488749fd6606c3f33fc984c2ae2", size = 2775825, upload-time = "2025-04-09T02:57:43.442Z" }, + { url = "https://files.pythonhosted.org/packages/95/9e/63c549f37136e892f006260c3e2613d09d5120672378191f2dc387ba65a2/numba-0.61.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:49c980e4171948ffebf6b9a2520ea81feed113c1f4890747ba7f59e74be84b1b", size = 2778695, upload-time = "2025-04-09T02:57:44.968Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/8740616c8436c86c1b9a62e72cb891177d2c34c2d24ddcde4c390371bf4c/numba-0.61.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3945615cd73c2c7eba2a85ccc9c1730c21cd3958bfcf5a44302abae0fb07bb60", size = 3829227, upload-time = "2025-04-09T02:57:46.63Z" }, + { url = "https://files.pythonhosted.org/packages/fc/06/66e99ae06507c31d15ff3ecd1f108f2f59e18b6e08662cd5f8a5853fbd18/numba-0.61.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbfdf4eca202cebade0b7d43896978e146f39398909a42941c9303f82f403a18", size = 3523422, upload-time = "2025-04-09T02:57:48.222Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a4/2b309a6a9f6d4d8cfba583401c7c2f9ff887adb5d54d8e2e130274c0973f/numba-0.61.2-cp311-cp311-win_amd64.whl", hash = "sha256:76bcec9f46259cedf888041b9886e257ae101c6268261b19fda8cfbc52bec9d1", size = 2831505, upload-time = "2025-04-09T02:57:50.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a0/c6b7b9c615cfa3b98c4c63f4316e3f6b3bbe2387740277006551784218cd/numba-0.61.2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:34fba9406078bac7ab052efbf0d13939426c753ad72946baaa5bf9ae0ebb8dd2", size = 2776626, upload-time = "2025-04-09T02:57:51.857Z" }, + { url = "https://files.pythonhosted.org/packages/92/4a/fe4e3c2ecad72d88f5f8cd04e7f7cff49e718398a2fac02d2947480a00ca/numba-0.61.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ddce10009bc097b080fc96876d14c051cc0c7679e99de3e0af59014dab7dfe8", size = 2779287, upload-time = "2025-04-09T02:57:53.658Z" }, + { url = "https://files.pythonhosted.org/packages/9a/2d/e518df036feab381c23a624dac47f8445ac55686ec7f11083655eb707da3/numba-0.61.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b1bb509d01f23d70325d3a5a0e237cbc9544dd50e50588bc581ba860c213546", size = 3885928, upload-time = "2025-04-09T02:57:55.206Z" }, + { url = "https://files.pythonhosted.org/packages/10/0f/23cced68ead67b75d77cfcca3df4991d1855c897ee0ff3fe25a56ed82108/numba-0.61.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48a53a3de8f8793526cbe330f2a39fe9a6638efcbf11bd63f3d2f9757ae345cd", size = 3577115, upload-time = "2025-04-09T02:57:56.818Z" }, + { url = "https://files.pythonhosted.org/packages/68/1d/ddb3e704c5a8fb90142bf9dc195c27db02a08a99f037395503bfbc1d14b3/numba-0.61.2-cp312-cp312-win_amd64.whl", hash = "sha256:97cf4f12c728cf77c9c1d7c23707e4d8fb4632b46275f8f3397de33e5877af18", size = 2831929, upload-time = "2025-04-09T02:57:58.45Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f3/0fe4c1b1f2569e8a18ad90c159298d862f96c3964392a20d74fc628aee44/numba-0.61.2-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:3a10a8fc9afac40b1eac55717cece1b8b1ac0b946f5065c89e00bde646b5b154", size = 2771785, upload-time = "2025-04-09T02:57:59.96Z" }, + { url = "https://files.pythonhosted.org/packages/e9/71/91b277d712e46bd5059f8a5866862ed1116091a7cb03bd2704ba8ebe015f/numba-0.61.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d3bcada3c9afba3bed413fba45845f2fb9cd0d2b27dd58a1be90257e293d140", size = 2773289, upload-time = "2025-04-09T02:58:01.435Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e0/5ea04e7ad2c39288c0f0f9e8d47638ad70f28e275d092733b5817cf243c9/numba-0.61.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bdbca73ad81fa196bd53dc12e3aaf1564ae036e0c125f237c7644fe64a4928ab", size = 3893918, upload-time = "2025-04-09T02:58:02.933Z" }, + { url = "https://files.pythonhosted.org/packages/17/58/064f4dcb7d7e9412f16ecf80ed753f92297e39f399c905389688cf950b81/numba-0.61.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5f154aaea625fb32cfbe3b80c5456d514d416fcdf79733dd69c0df3a11348e9e", size = 3584056, upload-time = "2025-04-09T02:58:04.538Z" }, + { url = "https://files.pythonhosted.org/packages/af/a4/6d3a0f2d3989e62a18749e1e9913d5fa4910bbb3e3311a035baea6caf26d/numba-0.61.2-cp313-cp313-win_amd64.whl", hash = "sha256:59321215e2e0ac5fa928a8020ab00b8e57cda8a97384963ac0dfa4d4e6aa54e7", size = 2831846, upload-time = "2025-04-09T02:58:06.125Z" }, +] + +[[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.11'", + "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 = "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 = "parso" +version = "0.8.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609, upload-time = "2024-04-05T09:43:55.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650, upload-time = "2024-04-05T09:43:53.299Z" }, +] + +[[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 = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + +[[package]] +name = "phy" +version = "2.0.0" +source = { editable = "." } +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 = "colorcet" }, + { name = "cython" }, + { 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 = "h5py" }, + { name = "ipykernel" }, + { name = "joblib" }, + { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "matplotlib", version = "3.10.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mtscomp" }, + { name = "numba", version = "0.60.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numba", version = "0.61.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "phylib" }, + { name = "pillow" }, + { name = "pip" }, + { name = "pyopengl" }, + { name = "qtconsole" }, + { name = "requests" }, + { name = "responses" }, + { name = "scikit-learn", version = "1.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "scikit-learn", version = "1.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { 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 = "setuptools" }, + { name = "tqdm" }, + { name = "traitlets" }, +] + +[package.optional-dependencies] +dev = [ + { name = "coverage" }, + { name = "coveralls" }, + { name = "memory-profiler" }, + { name = "mkdocs" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-qt" }, + { name = "ruff" }, +] +gui = [ + { name = "pyqt6" }, + { name = "pyqt6-webengine" }, +] +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 = "click" }, + { name = "colorcet" }, + { name = "coverage", marker = "extra == 'dev'" }, + { name = "coveralls", marker = "extra == 'dev'" }, + { name = "cython" }, + { name = "dask" }, + { name = "h5py" }, + { name = "ipykernel" }, + { name = "joblib" }, + { name = "matplotlib" }, + { name = "memory-profiler", marker = "extra == 'dev'" }, + { name = "mkdocs", marker = "extra == 'dev'" }, + { name = "mtscomp" }, + { name = "numba" }, + { name = "numpy" }, + { name = "phylib", git = "https://github.com/jesusdpa1/phylib_update.git" }, + { name = "pillow" }, + { name = "pip" }, + { name = "pyopengl", specifier = ">=3.1.9" }, + { name = "pyqt5", marker = "extra == 'qt5'", specifier = ">=5.12.0" }, + { name = "pyqt6", marker = "extra == 'gui'", specifier = ">=6.9.1" }, + { name = "pyqt6", marker = "extra == 'qt6'", specifier = ">=6.9.1" }, + { name = "pyqt6-webengine", marker = "extra == 'gui'", specifier = ">=6.9.0" }, + { 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 = "pytest-qt", marker = "extra == 'dev'" }, + { name = "qtconsole" }, + { name = "requests" }, + { name = "responses" }, + { name = "ruff", marker = "extra == 'dev'" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "setuptools" }, + { name = "tqdm" }, + { name = "traitlets" }, +] +provides-extras = ["dev", "qt5", "qt6", "gui", "qt"] + +[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 = "phylib" +version = "2.6.0" +source = { git = "https://github.com/jesusdpa1/phylib_update.git#10ab50bbf286687fa6c3cb03ba4065708df27009" } +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 = "requests" }, + { 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]] +name = "pillow" +version = "11.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707, upload-time = "2025-04-12T17:50:03.289Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/8b/b158ad57ed44d3cc54db8d68ad7c0a58b8fc0e4c7a3f995f9d62d5b464a1/pillow-11.2.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:d57a75d53922fc20c165016a20d9c44f73305e67c351bbc60d1adaf662e74047", size = 3198442, upload-time = "2025-04-12T17:47:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f8/bb5d956142f86c2d6cc36704943fa761f2d2e4c48b7436fd0a85c20f1713/pillow-11.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:127bf6ac4a5b58b3d32fc8289656f77f80567d65660bc46f72c0d77e6600cc95", size = 3030553, upload-time = "2025-04-12T17:47:13.153Z" }, + { url = "https://files.pythonhosted.org/packages/22/7f/0e413bb3e2aa797b9ca2c5c38cb2e2e45d88654e5b12da91ad446964cfae/pillow-11.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4ba4be812c7a40280629e55ae0b14a0aafa150dd6451297562e1764808bbe61", size = 4405503, upload-time = "2025-04-12T17:47:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b4/cc647f4d13f3eb837d3065824aa58b9bcf10821f029dc79955ee43f793bd/pillow-11.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8bd62331e5032bc396a93609982a9ab6b411c05078a52f5fe3cc59234a3abd1", size = 4490648, upload-time = "2025-04-12T17:47:17.37Z" }, + { url = "https://files.pythonhosted.org/packages/c2/6f/240b772a3b35cdd7384166461567aa6713799b4e78d180c555bd284844ea/pillow-11.2.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:562d11134c97a62fe3af29581f083033179f7ff435f78392565a1ad2d1c2c45c", size = 4508937, upload-time = "2025-04-12T17:47:19.066Z" }, + { url = "https://files.pythonhosted.org/packages/f3/5e/7ca9c815ade5fdca18853db86d812f2f188212792780208bdb37a0a6aef4/pillow-11.2.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:c97209e85b5be259994eb5b69ff50c5d20cca0f458ef9abd835e262d9d88b39d", size = 4599802, upload-time = "2025-04-12T17:47:21.404Z" }, + { url = "https://files.pythonhosted.org/packages/02/81/c3d9d38ce0c4878a77245d4cf2c46d45a4ad0f93000227910a46caff52f3/pillow-11.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0c3e6d0f59171dfa2e25d7116217543310908dfa2770aa64b8f87605f8cacc97", size = 4576717, upload-time = "2025-04-12T17:47:23.571Z" }, + { url = "https://files.pythonhosted.org/packages/42/49/52b719b89ac7da3185b8d29c94d0e6aec8140059e3d8adcaa46da3751180/pillow-11.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc1c3bc53befb6096b84165956e886b1729634a799e9d6329a0c512ab651e579", size = 4654874, upload-time = "2025-04-12T17:47:25.783Z" }, + { url = "https://files.pythonhosted.org/packages/5b/0b/ede75063ba6023798267023dc0d0401f13695d228194d2242d5a7ba2f964/pillow-11.2.1-cp310-cp310-win32.whl", hash = "sha256:312c77b7f07ab2139924d2639860e084ec2a13e72af54d4f08ac843a5fc9c79d", size = 2331717, upload-time = "2025-04-12T17:47:28.922Z" }, + { url = "https://files.pythonhosted.org/packages/ed/3c/9831da3edea527c2ed9a09f31a2c04e77cd705847f13b69ca60269eec370/pillow-11.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:9bc7ae48b8057a611e5fe9f853baa88093b9a76303937449397899385da06fad", size = 2676204, upload-time = "2025-04-12T17:47:31.283Z" }, + { url = "https://files.pythonhosted.org/packages/01/97/1f66ff8a1503d8cbfc5bae4dc99d54c6ec1e22ad2b946241365320caabc2/pillow-11.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:2728567e249cdd939f6cc3d1f049595c66e4187f3c34078cbc0a7d21c47482d2", size = 2414767, upload-time = "2025-04-12T17:47:34.655Z" }, + { url = "https://files.pythonhosted.org/packages/68/08/3fbf4b98924c73037a8e8b4c2c774784805e0fb4ebca6c5bb60795c40125/pillow-11.2.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35ca289f712ccfc699508c4658a1d14652e8033e9b69839edf83cbdd0ba39e70", size = 3198450, upload-time = "2025-04-12T17:47:37.135Z" }, + { url = "https://files.pythonhosted.org/packages/84/92/6505b1af3d2849d5e714fc75ba9e69b7255c05ee42383a35a4d58f576b16/pillow-11.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0409af9f829f87a2dfb7e259f78f317a5351f2045158be321fd135973fff7bf", size = 3030550, upload-time = "2025-04-12T17:47:39.345Z" }, + { url = "https://files.pythonhosted.org/packages/3c/8c/ac2f99d2a70ff966bc7eb13dacacfaab57c0549b2ffb351b6537c7840b12/pillow-11.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e5c5edee874dce4f653dbe59db7c73a600119fbea8d31f53423586ee2aafd7", size = 4415018, upload-time = "2025-04-12T17:47:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e3/0a58b5d838687f40891fff9cbaf8669f90c96b64dc8f91f87894413856c6/pillow-11.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b93a07e76d13bff9444f1a029e0af2964e654bfc2e2c2d46bfd080df5ad5f3d8", size = 4498006, upload-time = "2025-04-12T17:47:42.912Z" }, + { url = "https://files.pythonhosted.org/packages/21/f5/6ba14718135f08fbfa33308efe027dd02b781d3f1d5c471444a395933aac/pillow-11.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e6def7eed9e7fa90fde255afaf08060dc4b343bbe524a8f69bdd2a2f0018f600", size = 4517773, upload-time = "2025-04-12T17:47:44.611Z" }, + { url = "https://files.pythonhosted.org/packages/20/f2/805ad600fc59ebe4f1ba6129cd3a75fb0da126975c8579b8f57abeb61e80/pillow-11.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8f4f3724c068be008c08257207210c138d5f3731af6c155a81c2b09a9eb3a788", size = 4607069, upload-time = "2025-04-12T17:47:46.46Z" }, + { url = "https://files.pythonhosted.org/packages/71/6b/4ef8a288b4bb2e0180cba13ca0a519fa27aa982875882392b65131401099/pillow-11.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0a6709b47019dff32e678bc12c63008311b82b9327613f534e496dacaefb71e", size = 4583460, upload-time = "2025-04-12T17:47:49.255Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/f29c705a09cbc9e2a456590816e5c234382ae5d32584f451c3eb41a62062/pillow-11.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f6b0c664ccb879109ee3ca702a9272d877f4fcd21e5eb63c26422fd6e415365e", size = 4661304, upload-time = "2025-04-12T17:47:51.067Z" }, + { url = "https://files.pythonhosted.org/packages/6e/1a/c8217b6f2f73794a5e219fbad087701f412337ae6dbb956db37d69a9bc43/pillow-11.2.1-cp311-cp311-win32.whl", hash = "sha256:cc5d875d56e49f112b6def6813c4e3d3036d269c008bf8aef72cd08d20ca6df6", size = 2331809, upload-time = "2025-04-12T17:47:54.425Z" }, + { url = "https://files.pythonhosted.org/packages/e2/72/25a8f40170dc262e86e90f37cb72cb3de5e307f75bf4b02535a61afcd519/pillow-11.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:0f5c7eda47bf8e3c8a283762cab94e496ba977a420868cb819159980b6709193", size = 2676338, upload-time = "2025-04-12T17:47:56.535Z" }, + { url = "https://files.pythonhosted.org/packages/06/9e/76825e39efee61efea258b479391ca77d64dbd9e5804e4ad0fa453b4ba55/pillow-11.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:4d375eb838755f2528ac8cbc926c3e31cc49ca4ad0cf79cff48b20e30634a4a7", size = 2414918, upload-time = "2025-04-12T17:47:58.217Z" }, + { url = "https://files.pythonhosted.org/packages/c7/40/052610b15a1b8961f52537cc8326ca6a881408bc2bdad0d852edeb6ed33b/pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f", size = 3190185, upload-time = "2025-04-12T17:48:00.417Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7e/b86dbd35a5f938632093dc40d1682874c33dcfe832558fc80ca56bfcb774/pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b", size = 3030306, upload-time = "2025-04-12T17:48:02.391Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5c/467a161f9ed53e5eab51a42923c33051bf8d1a2af4626ac04f5166e58e0c/pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d", size = 4416121, upload-time = "2025-04-12T17:48:04.554Z" }, + { url = "https://files.pythonhosted.org/packages/62/73/972b7742e38ae0e2ac76ab137ca6005dcf877480da0d9d61d93b613065b4/pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4", size = 4501707, upload-time = "2025-04-12T17:48:06.831Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3a/427e4cb0b9e177efbc1a84798ed20498c4f233abde003c06d2650a6d60cb/pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d", size = 4522921, upload-time = "2025-04-12T17:48:09.229Z" }, + { url = "https://files.pythonhosted.org/packages/fe/7c/d8b1330458e4d2f3f45d9508796d7caf0c0d3764c00c823d10f6f1a3b76d/pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4", size = 4612523, upload-time = "2025-04-12T17:48:11.631Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2f/65738384e0b1acf451de5a573d8153fe84103772d139e1e0bdf1596be2ea/pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443", size = 4587836, upload-time = "2025-04-12T17:48:13.592Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c5/e795c9f2ddf3debb2dedd0df889f2fe4b053308bb59a3cc02a0cd144d641/pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c", size = 4669390, upload-time = "2025-04-12T17:48:15.938Z" }, + { url = "https://files.pythonhosted.org/packages/96/ae/ca0099a3995976a9fce2f423166f7bff9b12244afdc7520f6ed38911539a/pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3", size = 2332309, upload-time = "2025-04-12T17:48:17.885Z" }, + { url = "https://files.pythonhosted.org/packages/7c/18/24bff2ad716257fc03da964c5e8f05d9790a779a8895d6566e493ccf0189/pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941", size = 2676768, upload-time = "2025-04-12T17:48:19.655Z" }, + { url = "https://files.pythonhosted.org/packages/da/bb/e8d656c9543276517ee40184aaa39dcb41e683bca121022f9323ae11b39d/pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb", size = 2415087, upload-time = "2025-04-12T17:48:21.991Z" }, + { url = "https://files.pythonhosted.org/packages/36/9c/447528ee3776e7ab8897fe33697a7ff3f0475bb490c5ac1456a03dc57956/pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28", size = 3190098, upload-time = "2025-04-12T17:48:23.915Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/29d5cd052f7566a63e5b506fac9c60526e9ecc553825551333e1e18a4858/pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830", size = 3030166, upload-time = "2025-04-12T17:48:25.738Z" }, + { url = "https://files.pythonhosted.org/packages/71/5d/446ee132ad35e7600652133f9c2840b4799bbd8e4adba881284860da0a36/pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0", size = 4408674, upload-time = "2025-04-12T17:48:27.908Z" }, + { url = "https://files.pythonhosted.org/packages/69/5f/cbe509c0ddf91cc3a03bbacf40e5c2339c4912d16458fcb797bb47bcb269/pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1", size = 4496005, upload-time = "2025-04-12T17:48:29.888Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b3/dd4338d8fb8a5f312021f2977fb8198a1184893f9b00b02b75d565c33b51/pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f", size = 4518707, upload-time = "2025-04-12T17:48:31.874Z" }, + { url = "https://files.pythonhosted.org/packages/13/eb/2552ecebc0b887f539111c2cd241f538b8ff5891b8903dfe672e997529be/pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155", size = 4610008, upload-time = "2025-04-12T17:48:34.422Z" }, + { url = "https://files.pythonhosted.org/packages/72/d1/924ce51bea494cb6e7959522d69d7b1c7e74f6821d84c63c3dc430cbbf3b/pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14", size = 4585420, upload-time = "2025-04-12T17:48:37.641Z" }, + { url = "https://files.pythonhosted.org/packages/43/ab/8f81312d255d713b99ca37479a4cb4b0f48195e530cdc1611990eb8fd04b/pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b", size = 4667655, upload-time = "2025-04-12T17:48:39.652Z" }, + { url = "https://files.pythonhosted.org/packages/94/86/8f2e9d2dc3d308dfd137a07fe1cc478df0a23d42a6c4093b087e738e4827/pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2", size = 2332329, upload-time = "2025-04-12T17:48:41.765Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ec/1179083b8d6067a613e4d595359b5fdea65d0a3b7ad623fee906e1b3c4d2/pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691", size = 2676388, upload-time = "2025-04-12T17:48:43.625Z" }, + { url = "https://files.pythonhosted.org/packages/23/f1/2fc1e1e294de897df39fa8622d829b8828ddad938b0eaea256d65b84dd72/pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c", size = 2414950, upload-time = "2025-04-12T17:48:45.475Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3e/c328c48b3f0ead7bab765a84b4977acb29f101d10e4ef57a5e3400447c03/pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22", size = 3192759, upload-time = "2025-04-12T17:48:47.866Z" }, + { url = "https://files.pythonhosted.org/packages/18/0e/1c68532d833fc8b9f404d3a642991441d9058eccd5606eab31617f29b6d4/pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7", size = 3033284, upload-time = "2025-04-12T17:48:50.189Z" }, + { url = "https://files.pythonhosted.org/packages/b7/cb/6faf3fb1e7705fd2db74e070f3bf6f88693601b0ed8e81049a8266de4754/pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16", size = 4445826, upload-time = "2025-04-12T17:48:52.346Z" }, + { url = "https://files.pythonhosted.org/packages/07/94/8be03d50b70ca47fb434a358919d6a8d6580f282bbb7af7e4aa40103461d/pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b", size = 4527329, upload-time = "2025-04-12T17:48:54.403Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a4/bfe78777076dc405e3bd2080bc32da5ab3945b5a25dc5d8acaa9de64a162/pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406", size = 4549049, upload-time = "2025-04-12T17:48:56.383Z" }, + { url = "https://files.pythonhosted.org/packages/65/4d/eaf9068dc687c24979e977ce5677e253624bd8b616b286f543f0c1b91662/pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91", size = 4635408, upload-time = "2025-04-12T17:48:58.782Z" }, + { url = "https://files.pythonhosted.org/packages/1d/26/0fd443365d9c63bc79feb219f97d935cd4b93af28353cba78d8e77b61719/pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751", size = 4614863, upload-time = "2025-04-12T17:49:00.709Z" }, + { url = "https://files.pythonhosted.org/packages/49/65/dca4d2506be482c2c6641cacdba5c602bc76d8ceb618fd37de855653a419/pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9", size = 4692938, upload-time = "2025-04-12T17:49:02.946Z" }, + { url = "https://files.pythonhosted.org/packages/b3/92/1ca0c3f09233bd7decf8f7105a1c4e3162fb9142128c74adad0fb361b7eb/pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd", size = 2335774, upload-time = "2025-04-12T17:49:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ac/77525347cb43b83ae905ffe257bbe2cc6fd23acb9796639a1f56aa59d191/pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e", size = 2681895, upload-time = "2025-04-12T17:49:06.635Z" }, + { url = "https://files.pythonhosted.org/packages/67/32/32dc030cfa91ca0fc52baebbba2e009bb001122a1daa8b6a79ad830b38d3/pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681", size = 2417234, upload-time = "2025-04-12T17:49:08.399Z" }, + { url = "https://files.pythonhosted.org/packages/21/3a/c1835d1c7cf83559e95b4f4ed07ab0bb7acc689712adfce406b3f456e9fd/pillow-11.2.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:7491cf8a79b8eb867d419648fff2f83cb0b3891c8b36da92cc7f1931d46108c8", size = 3198391, upload-time = "2025-04-12T17:49:10.122Z" }, + { url = "https://files.pythonhosted.org/packages/b6/4d/dcb7a9af3fc1e8653267c38ed622605d9d1793349274b3ef7af06457e257/pillow-11.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b02d8f9cb83c52578a0b4beadba92e37d83a4ef11570a8688bbf43f4ca50909", size = 3030573, upload-time = "2025-04-12T17:49:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/9d/29/530ca098c1a1eb31d4e163d317d0e24e6d2ead907991c69ca5b663de1bc5/pillow-11.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:014ca0050c85003620526b0ac1ac53f56fc93af128f7546623cc8e31875ab928", size = 4398677, upload-time = "2025-04-12T17:49:13.861Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ee/0e5e51db34de1690264e5f30dcd25328c540aa11d50a3bc0b540e2a445b6/pillow-11.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3692b68c87096ac6308296d96354eddd25f98740c9d2ab54e1549d6c8aea9d79", size = 4484986, upload-time = "2025-04-12T17:49:15.948Z" }, + { url = "https://files.pythonhosted.org/packages/93/7d/bc723b41ce3d2c28532c47678ec988974f731b5c6fadd5b3a4fba9015e4f/pillow-11.2.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:f781dcb0bc9929adc77bad571b8621ecb1e4cdef86e940fe2e5b5ee24fd33b35", size = 4501897, upload-time = "2025-04-12T17:49:17.839Z" }, + { url = "https://files.pythonhosted.org/packages/be/0b/532e31abc7389617ddff12551af625a9b03cd61d2989fa595e43c470ec67/pillow-11.2.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:2b490402c96f907a166615e9a5afacf2519e28295f157ec3a2bb9bd57de638cb", size = 4592618, upload-time = "2025-04-12T17:49:19.7Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f0/21ed6499a6216fef753e2e2254a19d08bff3747108ba042422383f3e9faa/pillow-11.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dd6b20b93b3ccc9c1b597999209e4bc5cf2853f9ee66e3fc9a400a78733ffc9a", size = 4570493, upload-time = "2025-04-12T17:49:21.703Z" }, + { url = "https://files.pythonhosted.org/packages/68/de/17004ddb8ab855573fe1127ab0168d11378cdfe4a7ee2a792a70ff2e9ba7/pillow-11.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4b835d89c08a6c2ee7781b8dd0a30209a8012b5f09c0a665b65b0eb3560b6f36", size = 4647748, upload-time = "2025-04-12T17:49:23.579Z" }, + { url = "https://files.pythonhosted.org/packages/c7/23/82ecb486384bb3578115c509d4a00bb52f463ee700a5ca1be53da3c88c19/pillow-11.2.1-cp39-cp39-win32.whl", hash = "sha256:b10428b3416d4f9c61f94b494681280be7686bda15898a3a9e08eb66a6d92d67", size = 2331731, upload-time = "2025-04-12T17:49:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/58/bb/87efd58b3689537a623d44dbb2550ef0bb5ff6a62769707a0fe8b1a7bdeb/pillow-11.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:6ebce70c3f486acf7591a3d73431fa504a4e18a9b97ff27f5f47b7368e4b9dd1", size = 2676346, upload-time = "2025-04-12T17:49:27.342Z" }, + { url = "https://files.pythonhosted.org/packages/80/08/dc268475b22887b816e5dcfae31bce897f524b4646bab130c2142c9b2400/pillow-11.2.1-cp39-cp39-win_arm64.whl", hash = "sha256:c27476257b2fdcd7872d54cfd119b3a9ce4610fb85c8e32b70b42e3680a29a1e", size = 2414623, upload-time = "2025-04-12T17:49:29.139Z" }, + { url = "https://files.pythonhosted.org/packages/33/49/c8c21e4255b4f4a2c0c68ac18125d7f5460b109acc6dfdef1a24f9b960ef/pillow-11.2.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9b7b0d4fd2635f54ad82785d56bc0d94f147096493a79985d0ab57aedd563156", size = 3181727, upload-time = "2025-04-12T17:49:31.898Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f1/f7255c0838f8c1ef6d55b625cfb286835c17e8136ce4351c5577d02c443b/pillow-11.2.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:aa442755e31c64037aa7c1cb186e0b369f8416c567381852c63444dd666fb772", size = 2999833, upload-time = "2025-04-12T17:49:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/e2/57/9968114457bd131063da98d87790d080366218f64fa2943b65ac6739abb3/pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0d3348c95b766f54b76116d53d4cb171b52992a1027e7ca50c81b43b9d9e363", size = 3437472, upload-time = "2025-04-12T17:49:36.294Z" }, + { url = "https://files.pythonhosted.org/packages/b2/1b/e35d8a158e21372ecc48aac9c453518cfe23907bb82f950d6e1c72811eb0/pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85d27ea4c889342f7e35f6d56e7e1cb345632ad592e8c51b693d7b7556043ce0", size = 3459976, upload-time = "2025-04-12T17:49:38.988Z" }, + { url = "https://files.pythonhosted.org/packages/26/da/2c11d03b765efff0ccc473f1c4186dc2770110464f2177efaed9cf6fae01/pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bf2c33d6791c598142f00c9c4c7d47f6476731c31081331664eb26d6ab583e01", size = 3527133, upload-time = "2025-04-12T17:49:40.985Z" }, + { url = "https://files.pythonhosted.org/packages/79/1a/4e85bd7cadf78412c2a3069249a09c32ef3323650fd3005c97cca7aa21df/pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e616e7154c37669fc1dfc14584f11e284e05d1c650e1c0f972f281c4ccc53193", size = 3571555, upload-time = "2025-04-12T17:49:42.964Z" }, + { url = "https://files.pythonhosted.org/packages/69/03/239939915216de1e95e0ce2334bf17a7870ae185eb390fab6d706aadbfc0/pillow-11.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:39ad2e0f424394e3aebc40168845fee52df1394a4673a6ee512d840d14ab3013", size = 2674713, upload-time = "2025-04-12T17:49:44.944Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ad/2613c04633c7257d9481ab21d6b5364b59fc5d75faafd7cb8693523945a3/pillow-11.2.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80f1df8dbe9572b4b7abdfa17eb5d78dd620b1d55d9e25f834efdbee872d3aed", size = 3181734, upload-time = "2025-04-12T17:49:46.789Z" }, + { url = "https://files.pythonhosted.org/packages/a4/fd/dcdda4471ed667de57bb5405bb42d751e6cfdd4011a12c248b455c778e03/pillow-11.2.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ea926cfbc3957090becbcbbb65ad177161a2ff2ad578b5a6ec9bb1e1cd78753c", size = 2999841, upload-time = "2025-04-12T17:49:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/ac/89/8a2536e95e77432833f0db6fd72a8d310c8e4272a04461fb833eb021bf94/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:738db0e0941ca0376804d4de6a782c005245264edaa253ffce24e5a15cbdc7bd", size = 3437470, upload-time = "2025-04-12T17:49:50.831Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8f/abd47b73c60712f88e9eda32baced7bfc3e9bd6a7619bb64b93acff28c3e/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db98ab6565c69082ec9b0d4e40dd9f6181dab0dd236d26f7a50b8b9bfbd5076", size = 3460013, upload-time = "2025-04-12T17:49:53.278Z" }, + { url = "https://files.pythonhosted.org/packages/f6/20/5c0a0aa83b213b7a07ec01e71a3d6ea2cf4ad1d2c686cc0168173b6089e7/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:036e53f4170e270ddb8797d4c590e6dd14d28e15c7da375c18978045f7e6c37b", size = 3527165, upload-time = "2025-04-12T17:49:55.164Z" }, + { url = "https://files.pythonhosted.org/packages/58/0e/2abab98a72202d91146abc839e10c14f7cf36166f12838ea0c4db3ca6ecb/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:14f73f7c291279bd65fda51ee87affd7c1e097709f7fdd0188957a16c264601f", size = 3571586, upload-time = "2025-04-12T17:49:57.171Z" }, + { url = "https://files.pythonhosted.org/packages/21/2c/5e05f58658cf49b6667762cca03d6e7d85cededde2caf2ab37b81f80e574/pillow-11.2.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:208653868d5c9ecc2b327f9b9ef34e0e42a4cdd172c2988fd81d62d2bc9bc044", size = 2674751, upload-time = "2025-04-12T17:49:59.628Z" }, +] + +[[package]] +name = "pip" +version = "25.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/59/de/241caa0ca606f2ec5fe0c1f4261b0465df78d786a38da693864a116c37f4/pip-25.1.1.tar.gz", hash = "sha256:3de45d411d308d5054c2168185d8da7f9a2cd753dbac8acbfa88a8909ecd9077", size = 1940155, upload-time = "2025-05-02T15:14:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/a2/d40fb2460e883eca5199c62cfc2463fd261f760556ae6290f88488c362c0/pip-25.1.1-py3-none-any.whl", hash = "sha256:2913a38a2abf4ea6b64ab507bd9e967f3b53dc1ede74b01b0931e1ce548751af", size = 1825227, upload-time = "2025-05-02T15:13:59.102Z" }, +] + +[[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 = "prompt-toolkit" +version = "3.0.51" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940, upload-time = "2025-04-15T09:18:47.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" }, +] + +[[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 = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, +] + +[[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 = "pyopengl" +version = "3.1.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/42/71080db298df3ddb7e3090bfea8fd7c300894d8b10954c22f8719bd434eb/pyopengl-3.1.9.tar.gz", hash = "sha256:28ebd82c5f4491a418aeca9672dffb3adbe7d33b39eada4548a5b4e8c03f60c8", size = 1913642, upload-time = "2025-01-20T02:17:53.263Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/44/8634af40b0db528b5b37e901c0dc67321354880d251bf8965901d57693a5/PyOpenGL-3.1.9-py3-none-any.whl", hash = "sha256:15995fd3b0deb991376805da36137a4ae5aba6ddbb5e29ac1f35462d130a3f77", size = 3190341, upload-time = "2025-01-20T02:17:50.913Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608, upload-time = "2025-03-25T05:01:28.114Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload-time = "2025-03-25T05:01:24.908Z" }, +] + +[[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.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" }, +] + +[[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 = "pywin32" +version = "310" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/da/a5f38fffbba2fb99aa4aa905480ac4b8e83ca486659ac8c95bce47fb5276/pywin32-310-cp310-cp310-win32.whl", hash = "sha256:6dd97011efc8bf51d6793a82292419eba2c71cf8e7250cfac03bba284454abc1", size = 8848240, upload-time = "2025-03-17T00:55:46.783Z" }, + { url = "https://files.pythonhosted.org/packages/aa/fe/d873a773324fa565619ba555a82c9dabd677301720f3660a731a5d07e49a/pywin32-310-cp310-cp310-win_amd64.whl", hash = "sha256:c3e78706e4229b915a0821941a84e7ef420bf2b77e08c9dae3c76fd03fd2ae3d", size = 9601854, upload-time = "2025-03-17T00:55:48.783Z" }, + { url = "https://files.pythonhosted.org/packages/3c/84/1a8e3d7a15490d28a5d816efa229ecb4999cdc51a7c30dd8914f669093b8/pywin32-310-cp310-cp310-win_arm64.whl", hash = "sha256:33babed0cf0c92a6f94cc6cc13546ab24ee13e3e800e61ed87609ab91e4c8213", size = 8522963, upload-time = "2025-03-17T00:55:50.969Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b1/68aa2986129fb1011dabbe95f0136f44509afaf072b12b8f815905a39f33/pywin32-310-cp311-cp311-win32.whl", hash = "sha256:1e765f9564e83011a63321bb9d27ec456a0ed90d3732c4b2e312b855365ed8bd", size = 8784284, upload-time = "2025-03-17T00:55:53.124Z" }, + { url = "https://files.pythonhosted.org/packages/b3/bd/d1592635992dd8db5bb8ace0551bc3a769de1ac8850200cfa517e72739fb/pywin32-310-cp311-cp311-win_amd64.whl", hash = "sha256:126298077a9d7c95c53823934f000599f66ec9296b09167810eb24875f32689c", size = 9520748, upload-time = "2025-03-17T00:55:55.203Z" }, + { url = "https://files.pythonhosted.org/packages/90/b1/ac8b1ffce6603849eb45a91cf126c0fa5431f186c2e768bf56889c46f51c/pywin32-310-cp311-cp311-win_arm64.whl", hash = "sha256:19ec5fc9b1d51c4350be7bb00760ffce46e6c95eaf2f0b2f1150657b1a43c582", size = 8455941, upload-time = "2025-03-17T00:55:57.048Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ec/4fdbe47932f671d6e348474ea35ed94227fb5df56a7c30cbbb42cd396ed0/pywin32-310-cp312-cp312-win32.whl", hash = "sha256:8a75a5cc3893e83a108c05d82198880704c44bbaee4d06e442e471d3c9ea4f3d", size = 8796239, upload-time = "2025-03-17T00:55:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e5/b0627f8bb84e06991bea89ad8153a9e50ace40b2e1195d68e9dff6b03d0f/pywin32-310-cp312-cp312-win_amd64.whl", hash = "sha256:bf5c397c9a9a19a6f62f3fb821fbf36cac08f03770056711f765ec1503972060", size = 9503839, upload-time = "2025-03-17T00:56:00.8Z" }, + { url = "https://files.pythonhosted.org/packages/1f/32/9ccf53748df72301a89713936645a664ec001abd35ecc8578beda593d37d/pywin32-310-cp312-cp312-win_arm64.whl", hash = "sha256:2349cc906eae872d0663d4d6290d13b90621eaf78964bb1578632ff20e152966", size = 8459470, upload-time = "2025-03-17T00:56:02.601Z" }, + { url = "https://files.pythonhosted.org/packages/1c/09/9c1b978ffc4ae53999e89c19c77ba882d9fce476729f23ef55211ea1c034/pywin32-310-cp313-cp313-win32.whl", hash = "sha256:5d241a659c496ada3253cd01cfaa779b048e90ce4b2b38cd44168ad555ce74ab", size = 8794384, upload-time = "2025-03-17T00:56:04.383Z" }, + { url = "https://files.pythonhosted.org/packages/45/3c/b4640f740ffebadd5d34df35fecba0e1cfef8fde9f3e594df91c28ad9b50/pywin32-310-cp313-cp313-win_amd64.whl", hash = "sha256:667827eb3a90208ddbdcc9e860c81bde63a135710e21e4cb3348968e4bd5249e", size = 9503039, upload-time = "2025-03-17T00:56:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f4/f785020090fb050e7fb6d34b780f2231f302609dc964672f72bfaeb59a28/pywin32-310-cp313-cp313-win_arm64.whl", hash = "sha256:e308f831de771482b7cf692a1f308f8fca701b2d8f9dde6cc440c7da17e47b33", size = 8458152, upload-time = "2025-03-17T00:56:07.819Z" }, + { url = "https://files.pythonhosted.org/packages/a2/cd/d09d434630edb6a0c44ad5079611279a67530296cfe0451e003de7f449ff/pywin32-310-cp39-cp39-win32.whl", hash = "sha256:851c8d927af0d879221e616ae1f66145253537bbdd321a77e8ef701b443a9a1a", size = 8848099, upload-time = "2025-03-17T00:55:42.415Z" }, + { url = "https://files.pythonhosted.org/packages/93/ff/2a8c10315ffbdee7b3883ac0d1667e267ca8b3f6f640d81d43b87a82c0c7/pywin32-310-cp39-cp39-win_amd64.whl", hash = "sha256:96867217335559ac619f00ad70e513c0fcf84b8a3af9fc2bba3b59b97da70475", size = 9602031, upload-time = "2025-03-17T00:55:44.512Z" }, +] + +[[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 = "pyzmq" +version = "26.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/11/b9213d25230ac18a71b39b3723494e57adebe36e066397b961657b3b41c1/pyzmq-26.4.0.tar.gz", hash = "sha256:4bd13f85f80962f91a651a7356fe0472791a5f7a92f227822b5acf44795c626d", size = 278293, upload-time = "2025-04-04T12:05:44.049Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/b8/af1d814ffc3ff9730f9a970cbf216b6f078e5d251a25ef5201d7bc32a37c/pyzmq-26.4.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:0329bdf83e170ac133f44a233fc651f6ed66ef8e66693b5af7d54f45d1ef5918", size = 1339238, upload-time = "2025-04-04T12:03:07.022Z" }, + { url = "https://files.pythonhosted.org/packages/ee/e4/5aafed4886c264f2ea6064601ad39c5fc4e9b6539c6ebe598a859832eeee/pyzmq-26.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:398a825d2dea96227cf6460ce0a174cf7657d6f6827807d4d1ae9d0f9ae64315", size = 672848, upload-time = "2025-04-04T12:03:08.591Z" }, + { url = "https://files.pythonhosted.org/packages/79/39/026bf49c721cb42f1ef3ae0ee3d348212a7621d2adb739ba97599b6e4d50/pyzmq-26.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d52d62edc96787f5c1dfa6c6ccff9b581cfae5a70d94ec4c8da157656c73b5b", size = 911299, upload-time = "2025-04-04T12:03:10Z" }, + { url = "https://files.pythonhosted.org/packages/03/23/b41f936a9403b8f92325c823c0f264c6102a0687a99c820f1aaeb99c1def/pyzmq-26.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1410c3a3705db68d11eb2424d75894d41cff2f64d948ffe245dd97a9debfebf4", size = 867920, upload-time = "2025-04-04T12:03:11.311Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3e/2de5928cdadc2105e7c8f890cc5f404136b41ce5b6eae5902167f1d5641c/pyzmq-26.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:7dacb06a9c83b007cc01e8e5277f94c95c453c5851aac5e83efe93e72226353f", size = 862514, upload-time = "2025-04-04T12:03:13.013Z" }, + { url = "https://files.pythonhosted.org/packages/ce/57/109569514dd32e05a61d4382bc88980c95bfd2f02e58fea47ec0ccd96de1/pyzmq-26.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6bab961c8c9b3a4dc94d26e9b2cdf84de9918931d01d6ff38c721a83ab3c0ef5", size = 1204494, upload-time = "2025-04-04T12:03:14.795Z" }, + { url = "https://files.pythonhosted.org/packages/aa/02/dc51068ff2ca70350d1151833643a598625feac7b632372d229ceb4de3e1/pyzmq-26.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7a5c09413b924d96af2aa8b57e76b9b0058284d60e2fc3730ce0f979031d162a", size = 1514525, upload-time = "2025-04-04T12:03:16.246Z" }, + { url = "https://files.pythonhosted.org/packages/48/2a/a7d81873fff0645eb60afaec2b7c78a85a377af8f1d911aff045d8955bc7/pyzmq-26.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7d489ac234d38e57f458fdbd12a996bfe990ac028feaf6f3c1e81ff766513d3b", size = 1414659, upload-time = "2025-04-04T12:03:17.652Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ea/813af9c42ae21845c1ccfe495bd29c067622a621e85d7cda6bc437de8101/pyzmq-26.4.0-cp310-cp310-win32.whl", hash = "sha256:dea1c8db78fb1b4b7dc9f8e213d0af3fc8ecd2c51a1d5a3ca1cde1bda034a980", size = 580348, upload-time = "2025-04-04T12:03:19.384Z" }, + { url = "https://files.pythonhosted.org/packages/20/68/318666a89a565252c81d3fed7f3b4c54bd80fd55c6095988dfa2cd04a62b/pyzmq-26.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:fa59e1f5a224b5e04dc6c101d7186058efa68288c2d714aa12d27603ae93318b", size = 643838, upload-time = "2025-04-04T12:03:20.795Z" }, + { url = "https://files.pythonhosted.org/packages/91/f8/fb1a15b5f4ecd3e588bfde40c17d32ed84b735195b5c7d1d7ce88301a16f/pyzmq-26.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:a651fe2f447672f4a815e22e74630b6b1ec3a1ab670c95e5e5e28dcd4e69bbb5", size = 559565, upload-time = "2025-04-04T12:03:22.676Z" }, + { url = "https://files.pythonhosted.org/packages/32/6d/234e3b0aa82fd0290b1896e9992f56bdddf1f97266110be54d0177a9d2d9/pyzmq-26.4.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:bfcf82644c9b45ddd7cd2a041f3ff8dce4a0904429b74d73a439e8cab1bd9e54", size = 1339723, upload-time = "2025-04-04T12:03:24.358Z" }, + { url = "https://files.pythonhosted.org/packages/4f/11/6d561efe29ad83f7149a7cd48e498e539ed09019c6cd7ecc73f4cc725028/pyzmq-26.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9bcae3979b2654d5289d3490742378b2f3ce804b0b5fd42036074e2bf35b030", size = 672645, upload-time = "2025-04-04T12:03:25.693Z" }, + { url = "https://files.pythonhosted.org/packages/19/fd/81bfe3e23f418644660bad1a90f0d22f0b3eebe33dd65a79385530bceb3d/pyzmq-26.4.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccdff8ac4246b6fb60dcf3982dfaeeff5dd04f36051fe0632748fc0aa0679c01", size = 910133, upload-time = "2025-04-04T12:03:27.625Z" }, + { url = "https://files.pythonhosted.org/packages/97/68/321b9c775595ea3df832a9516252b653fe32818db66fdc8fa31c9b9fce37/pyzmq-26.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4550af385b442dc2d55ab7717837812799d3674cb12f9a3aa897611839c18e9e", size = 867428, upload-time = "2025-04-04T12:03:29.004Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6e/159cbf2055ef36aa2aa297e01b24523176e5b48ead283c23a94179fb2ba2/pyzmq-26.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:2f9f7ffe9db1187a253fca95191854b3fda24696f086e8789d1d449308a34b88", size = 862409, upload-time = "2025-04-04T12:03:31.032Z" }, + { url = "https://files.pythonhosted.org/packages/05/1c/45fb8db7be5a7d0cadea1070a9cbded5199a2d578de2208197e592f219bd/pyzmq-26.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3709c9ff7ba61589b7372923fd82b99a81932b592a5c7f1a24147c91da9a68d6", size = 1205007, upload-time = "2025-04-04T12:03:32.687Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fa/658c7f583af6498b463f2fa600f34e298e1b330886f82f1feba0dc2dd6c3/pyzmq-26.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f8f3c30fb2d26ae5ce36b59768ba60fb72507ea9efc72f8f69fa088450cff1df", size = 1514599, upload-time = "2025-04-04T12:03:34.084Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d7/44d641522353ce0a2bbd150379cb5ec32f7120944e6bfba4846586945658/pyzmq-26.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:382a4a48c8080e273427fc692037e3f7d2851959ffe40864f2db32646eeb3cef", size = 1414546, upload-time = "2025-04-04T12:03:35.478Z" }, + { url = "https://files.pythonhosted.org/packages/72/76/c8ed7263218b3d1e9bce07b9058502024188bd52cc0b0a267a9513b431fc/pyzmq-26.4.0-cp311-cp311-win32.whl", hash = "sha256:d56aad0517d4c09e3b4f15adebba8f6372c5102c27742a5bdbfc74a7dceb8fca", size = 579247, upload-time = "2025-04-04T12:03:36.846Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d0/2d9abfa2571a0b1a67c0ada79a8aa1ba1cce57992d80f771abcdf99bb32c/pyzmq-26.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:963977ac8baed7058c1e126014f3fe58b3773f45c78cce7af5c26c09b6823896", size = 644727, upload-time = "2025-04-04T12:03:38.578Z" }, + { url = "https://files.pythonhosted.org/packages/0d/d1/c8ad82393be6ccedfc3c9f3adb07f8f3976e3c4802640fe3f71441941e70/pyzmq-26.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:c0c8e8cadc81e44cc5088fcd53b9b3b4ce9344815f6c4a03aec653509296fae3", size = 559942, upload-time = "2025-04-04T12:03:40.143Z" }, + { url = "https://files.pythonhosted.org/packages/10/44/a778555ebfdf6c7fc00816aad12d185d10a74d975800341b1bc36bad1187/pyzmq-26.4.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:5227cb8da4b6f68acfd48d20c588197fd67745c278827d5238c707daf579227b", size = 1341586, upload-time = "2025-04-04T12:03:41.954Z" }, + { url = "https://files.pythonhosted.org/packages/9c/4f/f3a58dc69ac757e5103be3bd41fb78721a5e17da7cc617ddb56d973a365c/pyzmq-26.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1c07a7fa7f7ba86554a2b1bef198c9fed570c08ee062fd2fd6a4dcacd45f905", size = 665880, upload-time = "2025-04-04T12:03:43.45Z" }, + { url = "https://files.pythonhosted.org/packages/fe/45/50230bcfb3ae5cb98bee683b6edeba1919f2565d7cc1851d3c38e2260795/pyzmq-26.4.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae775fa83f52f52de73183f7ef5395186f7105d5ed65b1ae65ba27cb1260de2b", size = 902216, upload-time = "2025-04-04T12:03:45.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/59/56bbdc5689be5e13727491ad2ba5efd7cd564365750514f9bc8f212eef82/pyzmq-26.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66c760d0226ebd52f1e6b644a9e839b5db1e107a23f2fcd46ec0569a4fdd4e63", size = 859814, upload-time = "2025-04-04T12:03:47.188Z" }, + { url = "https://files.pythonhosted.org/packages/81/b1/57db58cfc8af592ce94f40649bd1804369c05b2190e4cbc0a2dad572baeb/pyzmq-26.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ef8c6ecc1d520debc147173eaa3765d53f06cd8dbe7bd377064cdbc53ab456f5", size = 855889, upload-time = "2025-04-04T12:03:49.223Z" }, + { url = "https://files.pythonhosted.org/packages/e8/92/47542e629cbac8f221c230a6d0f38dd3d9cff9f6f589ed45fdf572ffd726/pyzmq-26.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3150ef4084e163dec29ae667b10d96aad309b668fac6810c9e8c27cf543d6e0b", size = 1197153, upload-time = "2025-04-04T12:03:50.591Z" }, + { url = "https://files.pythonhosted.org/packages/07/e5/b10a979d1d565d54410afc87499b16c96b4a181af46e7645ab4831b1088c/pyzmq-26.4.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4448c9e55bf8329fa1dcedd32f661bf611214fa70c8e02fee4347bc589d39a84", size = 1507352, upload-time = "2025-04-04T12:03:52.473Z" }, + { url = "https://files.pythonhosted.org/packages/ab/58/5a23db84507ab9c01c04b1232a7a763be66e992aa2e66498521bbbc72a71/pyzmq-26.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e07dde3647afb084d985310d067a3efa6efad0621ee10826f2cb2f9a31b89d2f", size = 1406834, upload-time = "2025-04-04T12:03:54Z" }, + { url = "https://files.pythonhosted.org/packages/22/74/aaa837b331580c13b79ac39396601fb361454ee184ca85e8861914769b99/pyzmq-26.4.0-cp312-cp312-win32.whl", hash = "sha256:ba034a32ecf9af72adfa5ee383ad0fd4f4e38cdb62b13624278ef768fe5b5b44", size = 577992, upload-time = "2025-04-04T12:03:55.815Z" }, + { url = "https://files.pythonhosted.org/packages/30/0f/55f8c02c182856743b82dde46b2dc3e314edda7f1098c12a8227eeda0833/pyzmq-26.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:056a97aab4064f526ecb32f4343917a4022a5d9efb6b9df990ff72e1879e40be", size = 640466, upload-time = "2025-04-04T12:03:57.231Z" }, + { url = "https://files.pythonhosted.org/packages/e4/29/073779afc3ef6f830b8de95026ef20b2d1ec22d0324d767748d806e57379/pyzmq-26.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f23c750e485ce1eb639dbd576d27d168595908aa2d60b149e2d9e34c9df40e0", size = 556342, upload-time = "2025-04-04T12:03:59.218Z" }, + { url = "https://files.pythonhosted.org/packages/d7/20/fb2c92542488db70f833b92893769a569458311a76474bda89dc4264bd18/pyzmq-26.4.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:c43fac689880f5174d6fc864857d1247fe5cfa22b09ed058a344ca92bf5301e3", size = 1339484, upload-time = "2025-04-04T12:04:00.671Z" }, + { url = "https://files.pythonhosted.org/packages/58/29/2f06b9cabda3a6ea2c10f43e67ded3e47fc25c54822e2506dfb8325155d4/pyzmq-26.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:902aca7eba477657c5fb81c808318460328758e8367ecdd1964b6330c73cae43", size = 666106, upload-time = "2025-04-04T12:04:02.366Z" }, + { url = "https://files.pythonhosted.org/packages/77/e4/dcf62bd29e5e190bd21bfccaa4f3386e01bf40d948c239239c2f1e726729/pyzmq-26.4.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5e48a830bfd152fe17fbdeaf99ac5271aa4122521bf0d275b6b24e52ef35eb6", size = 902056, upload-time = "2025-04-04T12:04:03.919Z" }, + { url = "https://files.pythonhosted.org/packages/1a/cf/b36b3d7aea236087d20189bec1a87eeb2b66009731d7055e5c65f845cdba/pyzmq-26.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31be2b6de98c824c06f5574331f805707c667dc8f60cb18580b7de078479891e", size = 860148, upload-time = "2025-04-04T12:04:05.581Z" }, + { url = "https://files.pythonhosted.org/packages/18/a6/f048826bc87528c208e90604c3bf573801e54bd91e390cbd2dfa860e82dc/pyzmq-26.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6332452034be001bbf3206ac59c0d2a7713de5f25bb38b06519fc6967b7cf771", size = 855983, upload-time = "2025-04-04T12:04:07.096Z" }, + { url = "https://files.pythonhosted.org/packages/0a/27/454d34ab6a1d9772a36add22f17f6b85baf7c16e14325fa29e7202ca8ee8/pyzmq-26.4.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:da8c0f5dd352136853e6a09b1b986ee5278dfddfebd30515e16eae425c872b30", size = 1197274, upload-time = "2025-04-04T12:04:08.523Z" }, + { url = "https://files.pythonhosted.org/packages/f4/3d/7abfeab6b83ad38aa34cbd57c6fc29752c391e3954fd12848bd8d2ec0df6/pyzmq-26.4.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:f4ccc1a0a2c9806dda2a2dd118a3b7b681e448f3bb354056cad44a65169f6d86", size = 1507120, upload-time = "2025-04-04T12:04:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/13/ff/bc8d21dbb9bc8705126e875438a1969c4f77e03fc8565d6901c7933a3d01/pyzmq-26.4.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1c0b5fceadbab461578daf8d1dcc918ebe7ddd2952f748cf30c7cf2de5d51101", size = 1406738, upload-time = "2025-04-04T12:04:12.509Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5d/d4cd85b24de71d84d81229e3bbb13392b2698432cf8fdcea5afda253d587/pyzmq-26.4.0-cp313-cp313-win32.whl", hash = "sha256:28e2b0ff5ba4b3dd11062d905682bad33385cfa3cc03e81abd7f0822263e6637", size = 577826, upload-time = "2025-04-04T12:04:14.289Z" }, + { url = "https://files.pythonhosted.org/packages/c6/6c/f289c1789d7bb6e5a3b3bef7b2a55089b8561d17132be7d960d3ff33b14e/pyzmq-26.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:23ecc9d241004c10e8b4f49d12ac064cd7000e1643343944a10df98e57bc544b", size = 640406, upload-time = "2025-04-04T12:04:15.757Z" }, + { url = "https://files.pythonhosted.org/packages/b3/99/676b8851cb955eb5236a0c1e9ec679ea5ede092bf8bf2c8a68d7e965cac3/pyzmq-26.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:1edb0385c7f025045d6e0f759d4d3afe43c17a3d898914ec6582e6f464203c08", size = 556216, upload-time = "2025-04-04T12:04:17.212Z" }, + { url = "https://files.pythonhosted.org/packages/65/c2/1fac340de9d7df71efc59d9c50fc7a635a77b103392d1842898dd023afcb/pyzmq-26.4.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:93a29e882b2ba1db86ba5dd5e88e18e0ac6b627026c5cfbec9983422011b82d4", size = 1333769, upload-time = "2025-04-04T12:04:18.665Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c7/6c03637e8d742c3b00bec4f5e4cd9d1c01b2f3694c6f140742e93ca637ed/pyzmq-26.4.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb45684f276f57110bb89e4300c00f1233ca631f08f5f42528a5c408a79efc4a", size = 658826, upload-time = "2025-04-04T12:04:20.405Z" }, + { url = "https://files.pythonhosted.org/packages/a5/97/a8dca65913c0f78e0545af2bb5078aebfc142ca7d91cdaffa1fbc73e5dbd/pyzmq-26.4.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f72073e75260cb301aad4258ad6150fa7f57c719b3f498cb91e31df16784d89b", size = 891650, upload-time = "2025-04-04T12:04:22.413Z" }, + { url = "https://files.pythonhosted.org/packages/7d/7e/f63af1031eb060bf02d033732b910fe48548dcfdbe9c785e9f74a6cc6ae4/pyzmq-26.4.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be37e24b13026cfedd233bcbbccd8c0bcd2fdd186216094d095f60076201538d", size = 849776, upload-time = "2025-04-04T12:04:23.959Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fa/1a009ce582802a895c0d5fe9413f029c940a0a8ee828657a3bb0acffd88b/pyzmq-26.4.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:237b283044934d26f1eeff4075f751b05d2f3ed42a257fc44386d00df6a270cf", size = 842516, upload-time = "2025-04-04T12:04:25.449Z" }, + { url = "https://files.pythonhosted.org/packages/6e/bc/f88b0bad0f7a7f500547d71e99f10336f2314e525d4ebf576a1ea4a1d903/pyzmq-26.4.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:b30f862f6768b17040929a68432c8a8be77780317f45a353cb17e423127d250c", size = 1189183, upload-time = "2025-04-04T12:04:27.035Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8c/db446a3dd9cf894406dec2e61eeffaa3c07c3abb783deaebb9812c4af6a5/pyzmq-26.4.0-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:c80fcd3504232f13617c6ab501124d373e4895424e65de8b72042333316f64a8", size = 1495501, upload-time = "2025-04-04T12:04:28.833Z" }, + { url = "https://files.pythonhosted.org/packages/05/4c/bf3cad0d64c3214ac881299c4562b815f05d503bccc513e3fd4fdc6f67e4/pyzmq-26.4.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:26a2a7451606b87f67cdeca2c2789d86f605da08b4bd616b1a9981605ca3a364", size = 1395540, upload-time = "2025-04-04T12:04:30.562Z" }, + { url = "https://files.pythonhosted.org/packages/06/91/21d3af57bc77e86e9d1e5384f256fd25cdb4c8eed4c45c8119da8120915f/pyzmq-26.4.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:a88643de8abd000ce99ca72056a1a2ae15881ee365ecb24dd1d9111e43d57842", size = 1340634, upload-time = "2025-04-04T12:04:47.661Z" }, + { url = "https://files.pythonhosted.org/packages/54/e6/58cd825023e998a0e49db7322b3211e6cf93f0796710b77d1496304c10d1/pyzmq-26.4.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0a744ce209ecb557406fb928f3c8c55ce79b16c3eeb682da38ef5059a9af0848", size = 907880, upload-time = "2025-04-04T12:04:49.294Z" }, + { url = "https://files.pythonhosted.org/packages/72/83/619e44a766ef738cb7e8ed8e5a54565627801bdb027ca6dfb70762385617/pyzmq-26.4.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9434540f333332224ecb02ee6278b6c6f11ea1266b48526e73c903119b2f420f", size = 863003, upload-time = "2025-04-04T12:04:51Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6a/a59af31320598bdc63d2c5a3181d14a89673c2c794540678285482e8a342/pyzmq-26.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6c6f0a23e55cd38d27d4c89add963294ea091ebcb104d7fdab0f093bc5abb1c", size = 673432, upload-time = "2025-04-04T12:04:52.611Z" }, + { url = "https://files.pythonhosted.org/packages/29/ae/64dd6c18b08ce2cb009c60f11cf01c87f323acd80344d8b059c0304a7370/pyzmq-26.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6145df55dc2309f6ef72d70576dcd5aabb0fd373311613fe85a5e547c722b780", size = 1205221, upload-time = "2025-04-04T12:04:54.31Z" }, + { url = "https://files.pythonhosted.org/packages/d0/0b/c583ab750957b025244a66948831bc9ca486d11c820da4626caf6480ee1a/pyzmq-26.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2ea81823840ef8c56e5d2f9918e4d571236294fea4d1842b302aebffb9e40997", size = 1515299, upload-time = "2025-04-04T12:04:56.063Z" }, + { url = "https://files.pythonhosted.org/packages/22/ba/95ba76292c49dd9c6dff1f127b4867033020b708d101cba6e4fc5a3d166d/pyzmq-26.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cc2abc385dc37835445abe206524fbc0c9e3fce87631dfaa90918a1ba8f425eb", size = 1415366, upload-time = "2025-04-04T12:04:58.241Z" }, + { url = "https://files.pythonhosted.org/packages/6e/65/51abe36169effda26ac7400ffac96f463e09dff40d344cdc2629d9a59162/pyzmq-26.4.0-cp39-cp39-win32.whl", hash = "sha256:41a2508fe7bed4c76b4cf55aacfb8733926f59d440d9ae2b81ee8220633b4d12", size = 580773, upload-time = "2025-04-04T12:04:59.786Z" }, + { url = "https://files.pythonhosted.org/packages/89/68/d9ac94086c63a0ed8d73e9e8aec54b39f481696698a5a939a7207629fb30/pyzmq-26.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:d4000e8255d6cbce38982e5622ebb90823f3409b7ffe8aeae4337ef7d6d2612a", size = 644340, upload-time = "2025-04-04T12:05:01.389Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8f/66c261d657c1b0791ee5b372c90b1646b453adb581fcdc1dc5c94e5b03e3/pyzmq-26.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:b4f6919d9c120488246bdc2a2f96662fa80d67b35bd6d66218f457e722b3ff64", size = 560075, upload-time = "2025-04-04T12:05:02.975Z" }, + { url = "https://files.pythonhosted.org/packages/47/03/96004704a84095f493be8d2b476641f5c967b269390173f85488a53c1c13/pyzmq-26.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:98d948288ce893a2edc5ec3c438fe8de2daa5bbbd6e2e865ec5f966e237084ba", size = 834408, upload-time = "2025-04-04T12:05:04.569Z" }, + { url = "https://files.pythonhosted.org/packages/e4/7f/68d8f3034a20505db7551cb2260248be28ca66d537a1ac9a257913d778e4/pyzmq-26.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9f34f5c9e0203ece706a1003f1492a56c06c0632d86cb77bcfe77b56aacf27b", size = 569580, upload-time = "2025-04-04T12:05:06.283Z" }, + { url = "https://files.pythonhosted.org/packages/9b/a6/2b0d6801ec33f2b2a19dd8d02e0a1e8701000fec72926e6787363567d30c/pyzmq-26.4.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80c9b48aef586ff8b698359ce22f9508937c799cc1d2c9c2f7c95996f2300c94", size = 798250, upload-time = "2025-04-04T12:05:07.88Z" }, + { url = "https://files.pythonhosted.org/packages/96/2a/0322b3437de977dcac8a755d6d7ce6ec5238de78e2e2d9353730b297cf12/pyzmq-26.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3f2a5b74009fd50b53b26f65daff23e9853e79aa86e0aa08a53a7628d92d44a", size = 756758, upload-time = "2025-04-04T12:05:09.483Z" }, + { url = "https://files.pythonhosted.org/packages/c2/33/43704f066369416d65549ccee366cc19153911bec0154da7c6b41fca7e78/pyzmq-26.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:61c5f93d7622d84cb3092d7f6398ffc77654c346545313a3737e266fc11a3beb", size = 555371, upload-time = "2025-04-04T12:05:11.062Z" }, + { url = "https://files.pythonhosted.org/packages/04/52/a70fcd5592715702248306d8e1729c10742c2eac44529984413b05c68658/pyzmq-26.4.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4478b14cb54a805088299c25a79f27eaf530564a7a4f72bf432a040042b554eb", size = 834405, upload-time = "2025-04-04T12:05:13.3Z" }, + { url = "https://files.pythonhosted.org/packages/25/f9/1a03f1accff16b3af1a6fa22cbf7ced074776abbf688b2e9cb4629700c62/pyzmq-26.4.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a28ac29c60e4ba84b5f58605ace8ad495414a724fe7aceb7cf06cd0598d04e1", size = 569578, upload-time = "2025-04-04T12:05:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/76/0c/3a633acd762aa6655fcb71fa841907eae0ab1e8582ff494b137266de341d/pyzmq-26.4.0-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43b03c1ceea27c6520124f4fb2ba9c647409b9abdf9a62388117148a90419494", size = 798248, upload-time = "2025-04-04T12:05:17.376Z" }, + { url = "https://files.pythonhosted.org/packages/cd/cc/6c99c84aa60ac1cc56747bed6be8ce6305b9b861d7475772e7a25ce019d3/pyzmq-26.4.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7731abd23a782851426d4e37deb2057bf9410848a4459b5ede4fe89342e687a9", size = 756757, upload-time = "2025-04-04T12:05:19.19Z" }, + { url = "https://files.pythonhosted.org/packages/13/9c/d8073bd898eb896e94c679abe82e47506e2b750eb261cf6010ced869797c/pyzmq-26.4.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a222ad02fbe80166b0526c038776e8042cd4e5f0dec1489a006a1df47e9040e0", size = 555371, upload-time = "2025-04-04T12:05:20.702Z" }, + { url = "https://files.pythonhosted.org/packages/af/b2/71a644b629e1a93ccae9e22a45aec9d23065dfcc24c399cb837f81cd08c2/pyzmq-26.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:552b0d2e39987733e1e9e948a0ced6ff75e0ea39ab1a1db2fc36eb60fd8760db", size = 834397, upload-time = "2025-04-04T12:05:31.217Z" }, + { url = "https://files.pythonhosted.org/packages/a9/dd/052a25651eaaff8f5fd652fb40a3abb400e71207db2d605cf6faf0eac598/pyzmq-26.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd670a8aa843f2ee637039bbd412e0d7294a5e588e1ecc9ad98b0cdc050259a4", size = 569571, upload-time = "2025-04-04T12:05:32.877Z" }, + { url = "https://files.pythonhosted.org/packages/a5/5d/201ca10b5d12ab187a418352c06d70c3e2087310af038b11056aba1359be/pyzmq-26.4.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d367b7b775a0e1e54a59a2ba3ed4d5e0a31566af97cc9154e34262777dab95ed", size = 798243, upload-time = "2025-04-04T12:05:34.91Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d4/2c64e54749536ad1633400f28d71e71e19375d00ce1fe9bb1123364dc927/pyzmq-26.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112af16c406e4a93df2caef49f884f4c2bb2b558b0b5577ef0b2465d15c1abc", size = 756751, upload-time = "2025-04-04T12:05:37.12Z" }, + { url = "https://files.pythonhosted.org/packages/08/e6/34d119af43d06a8dcd88bf7a62dac69597eaba52b49ecce76ff06b40f1fd/pyzmq-26.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c76c298683f82669cab0b6da59071f55238c039738297c69f187a542c6d40099", size = 745400, upload-time = "2025-04-04T12:05:40.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/49/b5e471d74a63318e51f30d329b17d2550bdededaab55baed2e2499de7ce4/pyzmq-26.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:49b6ca2e625b46f499fb081aaf7819a177f41eeb555acb05758aa97f4f95d147", size = 555367, upload-time = "2025-04-04T12:05:42.356Z" }, +] + +[[package]] +name = "qtconsole" +version = "5.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipykernel" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "qtpy" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/83/d2b11f2f737c276d8650a04ad3bf478d10fbcd55fe39f129cdb9e6843d31/qtconsole-5.6.1.tar.gz", hash = "sha256:5cad1c7e6c75d3ef8143857fd2ed28062b4b92b933c2cc328252d18a9cfd0be5", size = 435808, upload-time = "2024-10-28T23:59:26.449Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/8a/635610fb6131bc702229e2780d7b042416866ab78f8ed1ff24c4b23a2f4c/qtconsole-5.6.1-py3-none-any.whl", hash = "sha256:3d22490d9589bace566ad4f3455b61fa2209156f40e87e19e2c3cb64e9264950", size = 125035, upload-time = "2024-10-28T23:59:24.191Z" }, +] + +[[package]] +name = "qtpy" +version = "2.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/01/392eba83c8e47b946b929d7c46e0f04b35e9671f8bb6fc36b6f7945b4de8/qtpy-2.4.3.tar.gz", hash = "sha256:db744f7832e6d3da90568ba6ccbca3ee2b3b4a890c3d6fbbc63142f6e4cdf5bb", size = 66982, upload-time = "2025-02-11T15:09:25.759Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/76/37c0ccd5ab968a6a438f9c623aeecc84c202ab2fabc6a8fd927580c15b5a/QtPy-2.4.3-py3-none-any.whl", hash = "sha256:72095afe13673e017946cc258b8d5da43314197b741ed2890e563cf384b51aa1", size = 95045, upload-time = "2025-02-11T15:09:24.162Z" }, +] + +[[package]] +name = "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 = "scikit-learn" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "joblib", marker = "python_full_version < '3.10'" }, + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "scipy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "threadpoolctl", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/a5/4ae3b3a0755f7b35a280ac90b28817d1f380318973cff14075ab41ef50d9/scikit_learn-1.6.1.tar.gz", hash = "sha256:b4fc2525eca2c69a59260f583c56a7557c6ccdf8deafdba6e060f94c1c59738e", size = 7068312, upload-time = "2025-01-10T08:07:55.348Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/3a/f4597eb41049110b21ebcbb0bcb43e4035017545daa5eedcfeb45c08b9c5/scikit_learn-1.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d056391530ccd1e501056160e3c9673b4da4805eb67eb2bdf4e983e1f9c9204e", size = 12067702, upload-time = "2025-01-10T08:05:56.515Z" }, + { url = "https://files.pythonhosted.org/packages/37/19/0423e5e1fd1c6ec5be2352ba05a537a473c1677f8188b9306097d684b327/scikit_learn-1.6.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0c8d036eb937dbb568c6242fa598d551d88fb4399c0344d95c001980ec1c7d36", size = 11112765, upload-time = "2025-01-10T08:06:00.272Z" }, + { url = "https://files.pythonhosted.org/packages/70/95/d5cb2297a835b0f5fc9a77042b0a2d029866379091ab8b3f52cc62277808/scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8634c4bd21a2a813e0a7e3900464e6d593162a29dd35d25bdf0103b3fce60ed5", size = 12643991, upload-time = "2025-01-10T08:06:04.813Z" }, + { url = "https://files.pythonhosted.org/packages/b7/91/ab3c697188f224d658969f678be86b0968ccc52774c8ab4a86a07be13c25/scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:775da975a471c4f6f467725dff0ced5c7ac7bda5e9316b260225b48475279a1b", size = 13497182, upload-time = "2025-01-10T08:06:08.42Z" }, + { url = "https://files.pythonhosted.org/packages/17/04/d5d556b6c88886c092cc989433b2bab62488e0f0dafe616a1d5c9cb0efb1/scikit_learn-1.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:8a600c31592bd7dab31e1c61b9bbd6dea1b3433e67d264d17ce1017dbdce8002", size = 11125517, upload-time = "2025-01-10T08:06:12.783Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2a/e291c29670795406a824567d1dfc91db7b699799a002fdaa452bceea8f6e/scikit_learn-1.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72abc587c75234935e97d09aa4913a82f7b03ee0b74111dcc2881cba3c5a7b33", size = 12102620, upload-time = "2025-01-10T08:06:16.675Z" }, + { url = "https://files.pythonhosted.org/packages/25/92/ee1d7a00bb6b8c55755d4984fd82608603a3cc59959245068ce32e7fb808/scikit_learn-1.6.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b3b00cdc8f1317b5f33191df1386c0befd16625f49d979fe77a8d44cae82410d", size = 11116234, upload-time = "2025-01-10T08:06:21.83Z" }, + { url = "https://files.pythonhosted.org/packages/30/cd/ed4399485ef364bb25f388ab438e3724e60dc218c547a407b6e90ccccaef/scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc4765af3386811c3ca21638f63b9cf5ecf66261cc4815c1db3f1e7dc7b79db2", size = 12592155, upload-time = "2025-01-10T08:06:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/62fc9a5a659bb58a03cdd7e258956a5824bdc9b4bb3c5d932f55880be569/scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25fc636bdaf1cc2f4a124a116312d837148b5e10872147bdaf4887926b8c03d8", size = 13497069, upload-time = "2025-01-10T08:06:32.515Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a6/c5b78606743a1f28eae8f11973de6613a5ee87366796583fb74c67d54939/scikit_learn-1.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:fa909b1a36e000a03c382aade0bd2063fd5680ff8b8e501660c0f59f021a6415", size = 11139809, upload-time = "2025-01-10T08:06:35.514Z" }, + { url = "https://files.pythonhosted.org/packages/0a/18/c797c9b8c10380d05616db3bfb48e2a3358c767affd0857d56c2eb501caa/scikit_learn-1.6.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:926f207c804104677af4857b2c609940b743d04c4c35ce0ddc8ff4f053cddc1b", size = 12104516, upload-time = "2025-01-10T08:06:40.009Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b7/2e35f8e289ab70108f8cbb2e7a2208f0575dc704749721286519dcf35f6f/scikit_learn-1.6.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2c2cae262064e6a9b77eee1c8e768fc46aa0b8338c6a8297b9b6759720ec0ff2", size = 11167837, upload-time = "2025-01-10T08:06:43.305Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f6/ff7beaeb644bcad72bcfd5a03ff36d32ee4e53a8b29a639f11bcb65d06cd/scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1061b7c028a8663fb9a1a1baf9317b64a257fcb036dae5c8752b2abef31d136f", size = 12253728, upload-time = "2025-01-10T08:06:47.618Z" }, + { url = "https://files.pythonhosted.org/packages/29/7a/8bce8968883e9465de20be15542f4c7e221952441727c4dad24d534c6d99/scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e69fab4ebfc9c9b580a7a80111b43d214ab06250f8a7ef590a4edf72464dd86", size = 13147700, upload-time = "2025-01-10T08:06:50.888Z" }, + { url = "https://files.pythonhosted.org/packages/62/27/585859e72e117fe861c2079bcba35591a84f801e21bc1ab85bce6ce60305/scikit_learn-1.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:70b1d7e85b1c96383f872a519b3375f92f14731e279a7b4c6cfd650cf5dffc52", size = 11110613, upload-time = "2025-01-10T08:06:54.115Z" }, + { url = "https://files.pythonhosted.org/packages/2e/59/8eb1872ca87009bdcdb7f3cdc679ad557b992c12f4b61f9250659e592c63/scikit_learn-1.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ffa1e9e25b3d93990e74a4be2c2fc61ee5af85811562f1288d5d055880c4322", size = 12010001, upload-time = "2025-01-10T08:06:58.613Z" }, + { url = "https://files.pythonhosted.org/packages/9d/05/f2fc4effc5b32e525408524c982c468c29d22f828834f0625c5ef3d601be/scikit_learn-1.6.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dc5cf3d68c5a20ad6d571584c0750ec641cc46aeef1c1507be51300e6003a7e1", size = 11096360, upload-time = "2025-01-10T08:07:01.556Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e4/4195d52cf4f113573fb8ebc44ed5a81bd511a92c0228889125fac2f4c3d1/scikit_learn-1.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c06beb2e839ecc641366000ca84f3cf6fa9faa1777e29cf0c04be6e4d096a348", size = 12209004, upload-time = "2025-01-10T08:07:06.931Z" }, + { url = "https://files.pythonhosted.org/packages/94/be/47e16cdd1e7fcf97d95b3cb08bde1abb13e627861af427a3651fcb80b517/scikit_learn-1.6.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8ca8cb270fee8f1f76fa9bfd5c3507d60c6438bbee5687f81042e2bb98e5a97", size = 13171776, upload-time = "2025-01-10T08:07:11.715Z" }, + { url = "https://files.pythonhosted.org/packages/34/b0/ca92b90859070a1487827dbc672f998da95ce83edce1270fc23f96f1f61a/scikit_learn-1.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:7a1c43c8ec9fde528d664d947dc4c0789be4077a3647f232869f41d9bf50e0fb", size = 11071865, upload-time = "2025-01-10T08:07:16.088Z" }, + { url = "https://files.pythonhosted.org/packages/12/ae/993b0fb24a356e71e9a894e42b8a9eec528d4c70217353a1cd7a48bc25d4/scikit_learn-1.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a17c1dea1d56dcda2fac315712f3651a1fea86565b64b48fa1bc090249cbf236", size = 11955804, upload-time = "2025-01-10T08:07:20.385Z" }, + { url = "https://files.pythonhosted.org/packages/d6/54/32fa2ee591af44507eac86406fa6bba968d1eb22831494470d0a2e4a1eb1/scikit_learn-1.6.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6a7aa5f9908f0f28f4edaa6963c0a6183f1911e63a69aa03782f0d924c830a35", size = 11100530, upload-time = "2025-01-10T08:07:23.675Z" }, + { url = "https://files.pythonhosted.org/packages/3f/58/55856da1adec655bdce77b502e94a267bf40a8c0b89f8622837f89503b5a/scikit_learn-1.6.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0650e730afb87402baa88afbf31c07b84c98272622aaba002559b614600ca691", size = 12433852, upload-time = "2025-01-10T08:07:26.817Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4f/c83853af13901a574f8f13b645467285a48940f185b690936bb700a50863/scikit_learn-1.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:3f59fe08dc03ea158605170eb52b22a105f238a5d512c4470ddeca71feae8e5f", size = 11337256, upload-time = "2025-01-10T08:07:31.084Z" }, + { url = "https://files.pythonhosted.org/packages/d2/37/b305b759cc65829fe1b8853ff3e308b12cdd9d8884aa27840835560f2b42/scikit_learn-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6849dd3234e87f55dce1db34c89a810b489ead832aaf4d4550b7ea85628be6c1", size = 12101868, upload-time = "2025-01-10T08:07:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/83/74/f64379a4ed5879d9db744fe37cfe1978c07c66684d2439c3060d19a536d8/scikit_learn-1.6.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:e7be3fa5d2eb9be7d77c3734ff1d599151bb523674be9b834e8da6abe132f44e", size = 11144062, upload-time = "2025-01-10T08:07:37.67Z" }, + { url = "https://files.pythonhosted.org/packages/fd/dc/d5457e03dc9c971ce2b0d750e33148dd060fefb8b7dc71acd6054e4bb51b/scikit_learn-1.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44a17798172df1d3c1065e8fcf9019183f06c87609b49a124ebdf57ae6cb0107", size = 12693173, upload-time = "2025-01-10T08:07:42.713Z" }, + { url = "https://files.pythonhosted.org/packages/79/35/b1d2188967c3204c78fa79c9263668cf1b98060e8e58d1a730fe5b2317bb/scikit_learn-1.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b7a3b86e411e4bce21186e1c180d792f3d99223dcfa3b4f597ecc92fa1a422", size = 13518605, upload-time = "2025-01-10T08:07:46.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d8/8d603bdd26601f4b07e2363032b8565ab82eb857f93d86d0f7956fcf4523/scikit_learn-1.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:7a73d457070e3318e32bdb3aa79a8d990474f19035464dfd8bede2883ab5dc3b", size = 11155078, upload-time = "2025-01-10T08:07:51.376Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "joblib", 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 = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "threadpoolctl", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/3b/29fa87e76b1d7b3b77cc1fcbe82e6e6b8cd704410705b008822de530277c/scikit_learn-1.7.0.tar.gz", hash = "sha256:c01e869b15aec88e2cdb73d27f15bdbe03bce8e2fb43afbe77c45d399e73a5a3", size = 7178217, upload-time = "2025-06-05T22:02:46.703Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/70/e725b1da11e7e833f558eb4d3ea8b7ed7100edda26101df074f1ae778235/scikit_learn-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9fe7f51435f49d97bd41d724bb3e11eeb939882af9c29c931a8002c357e8cdd5", size = 11728006, upload-time = "2025-06-05T22:01:43.007Z" }, + { url = "https://files.pythonhosted.org/packages/32/aa/43874d372e9dc51eb361f5c2f0a4462915c9454563b3abb0d9457c66b7e9/scikit_learn-1.7.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d0c93294e1e1acbee2d029b1f2a064f26bd928b284938d51d412c22e0c977eb3", size = 10726255, upload-time = "2025-06-05T22:01:46.082Z" }, + { url = "https://files.pythonhosted.org/packages/f5/1a/da73cc18e00f0b9ae89f7e4463a02fb6e0569778120aeab138d9554ecef0/scikit_learn-1.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf3755f25f145186ad8c403312f74fb90df82a4dfa1af19dc96ef35f57237a94", size = 12205657, upload-time = "2025-06-05T22:01:48.729Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f6/800cb3243dd0137ca6d98df8c9d539eb567ba0a0a39ecd245c33fab93510/scikit_learn-1.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2726c8787933add436fb66fb63ad18e8ef342dfb39bbbd19dc1e83e8f828a85a", size = 12877290, upload-time = "2025-06-05T22:01:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/4c/bd/99c3ccb49946bd06318fe194a1c54fb7d57ac4fe1c2f4660d86b3a2adf64/scikit_learn-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:e2539bb58886a531b6e86a510c0348afaadd25005604ad35966a85c2ec378800", size = 10713211, upload-time = "2025-06-05T22:01:54.107Z" }, + { url = "https://files.pythonhosted.org/packages/5a/42/c6b41711c2bee01c4800ad8da2862c0b6d2956a399d23ce4d77f2ca7f0c7/scikit_learn-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ef09b1615e1ad04dc0d0054ad50634514818a8eb3ee3dee99af3bffc0ef5007", size = 11719657, upload-time = "2025-06-05T22:01:56.345Z" }, + { url = "https://files.pythonhosted.org/packages/a3/24/44acca76449e391b6b2522e67a63c0454b7c1f060531bdc6d0118fb40851/scikit_learn-1.7.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:7d7240c7b19edf6ed93403f43b0fcb0fe95b53bc0b17821f8fb88edab97085ef", size = 10712636, upload-time = "2025-06-05T22:01:59.093Z" }, + { url = "https://files.pythonhosted.org/packages/9f/1b/fcad1ccb29bdc9b96bcaa2ed8345d56afb77b16c0c47bafe392cc5d1d213/scikit_learn-1.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80bd3bd4e95381efc47073a720d4cbab485fc483966f1709f1fd559afac57ab8", size = 12242817, upload-time = "2025-06-05T22:02:01.43Z" }, + { url = "https://files.pythonhosted.org/packages/c6/38/48b75c3d8d268a3f19837cb8a89155ead6e97c6892bb64837183ea41db2b/scikit_learn-1.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dbe48d69aa38ecfc5a6cda6c5df5abef0c0ebdb2468e92437e2053f84abb8bc", size = 12873961, upload-time = "2025-06-05T22:02:03.951Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5a/ba91b8c57aa37dbd80d5ff958576a9a8c14317b04b671ae7f0d09b00993a/scikit_learn-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:8fa979313b2ffdfa049ed07252dc94038def3ecd49ea2a814db5401c07f1ecfa", size = 10717277, upload-time = "2025-06-05T22:02:06.77Z" }, + { url = "https://files.pythonhosted.org/packages/70/3a/bffab14e974a665a3ee2d79766e7389572ffcaad941a246931c824afcdb2/scikit_learn-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c2c7243d34aaede0efca7a5a96d67fddaebb4ad7e14a70991b9abee9dc5c0379", size = 11646758, upload-time = "2025-06-05T22:02:09.51Z" }, + { url = "https://files.pythonhosted.org/packages/58/d8/f3249232fa79a70cb40595282813e61453c1e76da3e1a44b77a63dd8d0cb/scikit_learn-1.7.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:9f39f6a811bf3f15177b66c82cbe0d7b1ebad9f190737dcdef77cfca1ea3c19c", size = 10673971, upload-time = "2025-06-05T22:02:12.217Z" }, + { url = "https://files.pythonhosted.org/packages/67/93/eb14c50533bea2f77758abe7d60a10057e5f2e2cdcf0a75a14c6bc19c734/scikit_learn-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63017a5f9a74963d24aac7590287149a8d0f1a0799bbe7173c0d8ba1523293c0", size = 11818428, upload-time = "2025-06-05T22:02:14.947Z" }, + { url = "https://files.pythonhosted.org/packages/08/17/804cc13b22a8663564bb0b55fb89e661a577e4e88a61a39740d58b909efe/scikit_learn-1.7.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b2f8a0b1e73e9a08b7cc498bb2aeab36cdc1f571f8ab2b35c6e5d1c7115d97d", size = 12505887, upload-time = "2025-06-05T22:02:17.824Z" }, + { url = "https://files.pythonhosted.org/packages/68/c7/4e956281a077f4835458c3f9656c666300282d5199039f26d9de1dabd9be/scikit_learn-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:34cc8d9d010d29fb2b7cbcd5ccc24ffdd80515f65fe9f1e4894ace36b267ce19", size = 10668129, upload-time = "2025-06-05T22:02:20.536Z" }, + { url = "https://files.pythonhosted.org/packages/9a/c3/a85dcccdaf1e807e6f067fa95788a6485b0491d9ea44fd4c812050d04f45/scikit_learn-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5b7974f1f32bc586c90145df51130e02267e4b7e77cab76165c76cf43faca0d9", size = 11559841, upload-time = "2025-06-05T22:02:23.308Z" }, + { url = "https://files.pythonhosted.org/packages/d8/57/eea0de1562cc52d3196eae51a68c5736a31949a465f0b6bb3579b2d80282/scikit_learn-1.7.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:014e07a23fe02e65f9392898143c542a50b6001dbe89cb867e19688e468d049b", size = 10616463, upload-time = "2025-06-05T22:02:26.068Z" }, + { url = "https://files.pythonhosted.org/packages/10/a4/39717ca669296dfc3a62928393168da88ac9d8cbec88b6321ffa62c6776f/scikit_learn-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7e7ced20582d3a5516fb6f405fd1d254e1f5ce712bfef2589f51326af6346e8", size = 11766512, upload-time = "2025-06-05T22:02:28.689Z" }, + { url = "https://files.pythonhosted.org/packages/d5/cd/a19722241d5f7b51e08351e1e82453e0057aeb7621b17805f31fcb57bb6c/scikit_learn-1.7.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1babf2511e6ffd695da7a983b4e4d6de45dce39577b26b721610711081850906", size = 12461075, upload-time = "2025-06-05T22:02:31.233Z" }, + { url = "https://files.pythonhosted.org/packages/f3/bc/282514272815c827a9acacbe5b99f4f1a4bc5961053719d319480aee0812/scikit_learn-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:5abd2acff939d5bd4701283f009b01496832d50ddafa83c90125a4e41c33e314", size = 10652517, upload-time = "2025-06-05T22:02:34.139Z" }, + { url = "https://files.pythonhosted.org/packages/ea/78/7357d12b2e4c6674175f9a09a3ba10498cde8340e622715bcc71e532981d/scikit_learn-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e39d95a929b112047c25b775035c8c234c5ca67e681ce60d12413afb501129f7", size = 12111822, upload-time = "2025-06-05T22:02:36.904Z" }, + { url = "https://files.pythonhosted.org/packages/d0/0c/9c3715393343f04232f9d81fe540eb3831d0b4ec351135a145855295110f/scikit_learn-1.7.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:0521cb460426c56fee7e07f9365b0f45ec8ca7b2d696534ac98bfb85e7ae4775", size = 11325286, upload-time = "2025-06-05T22:02:39.739Z" }, + { url = "https://files.pythonhosted.org/packages/64/e0/42282ad3dd70b7c1a5f65c412ac3841f6543502a8d6263cae7b466612dc9/scikit_learn-1.7.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:317ca9f83acbde2883bd6bb27116a741bfcb371369706b4f9973cf30e9a03b0d", size = 12380865, upload-time = "2025-06-05T22:02:42.137Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d0/3ef4ab2c6be4aa910445cd09c5ef0b44512e3de2cfb2112a88bb647d2cf7/scikit_learn-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:126c09740a6f016e815ab985b21e3a0656835414521c81fc1a8da78b679bdb75", size = 11549609, upload-time = "2025-06-05T22:02:44.483Z" }, +] + +[[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'" }, +] +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 = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[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 = "tornado" +version = "6.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/89/c72771c81d25d53fe33e3dca61c233b665b2780f21820ba6fd2c6793c12b/tornado-6.5.1.tar.gz", hash = "sha256:84ceece391e8eb9b2b95578db65e920d2a61070260594819589609ba9bc6308c", size = 509934, upload-time = "2025-05-22T18:15:38.788Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/89/f4532dee6843c9e0ebc4e28d4be04c67f54f60813e4bf73d595fe7567452/tornado-6.5.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d50065ba7fd11d3bd41bcad0825227cc9a95154bad83239357094c36708001f7", size = 441948, upload-time = "2025-05-22T18:15:20.862Z" }, + { url = "https://files.pythonhosted.org/packages/15/9a/557406b62cffa395d18772e0cdcf03bed2fff03b374677348eef9f6a3792/tornado-6.5.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9e9ca370f717997cb85606d074b0e5b247282cf5e2e1611568b8821afe0342d6", size = 440112, upload-time = "2025-05-22T18:15:22.591Z" }, + { url = "https://files.pythonhosted.org/packages/55/82/7721b7319013a3cf881f4dffa4f60ceff07b31b394e459984e7a36dc99ec/tornado-6.5.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b77e9dfa7ed69754a54c89d82ef746398be82f749df69c4d3abe75c4d1ff4888", size = 443672, upload-time = "2025-05-22T18:15:24.027Z" }, + { url = "https://files.pythonhosted.org/packages/7d/42/d11c4376e7d101171b94e03cef0cbce43e823ed6567ceda571f54cf6e3ce/tornado-6.5.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:253b76040ee3bab8bcf7ba9feb136436a3787208717a1fb9f2c16b744fba7331", size = 443019, upload-time = "2025-05-22T18:15:25.735Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f7/0c48ba992d875521ac761e6e04b0a1750f8150ae42ea26df1852d6a98942/tornado-6.5.1-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:308473f4cc5a76227157cdf904de33ac268af770b2c5f05ca6c1161d82fdd95e", size = 443252, upload-time = "2025-05-22T18:15:27.499Z" }, + { url = "https://files.pythonhosted.org/packages/89/46/d8d7413d11987e316df4ad42e16023cd62666a3c0dfa1518ffa30b8df06c/tornado-6.5.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:caec6314ce8a81cf69bd89909f4b633b9f523834dc1a352021775d45e51d9401", size = 443930, upload-time = "2025-05-22T18:15:29.299Z" }, + { url = "https://files.pythonhosted.org/packages/78/b2/f8049221c96a06df89bed68260e8ca94beca5ea532ffc63b1175ad31f9cc/tornado-6.5.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:13ce6e3396c24e2808774741331638ee6c2f50b114b97a55c5b442df65fd9692", size = 443351, upload-time = "2025-05-22T18:15:31.038Z" }, + { url = "https://files.pythonhosted.org/packages/76/ff/6a0079e65b326cc222a54720a748e04a4db246870c4da54ece4577bfa702/tornado-6.5.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5cae6145f4cdf5ab24744526cc0f55a17d76f02c98f4cff9daa08ae9a217448a", size = 443328, upload-time = "2025-05-22T18:15:32.426Z" }, + { url = "https://files.pythonhosted.org/packages/49/18/e3f902a1d21f14035b5bc6246a8c0f51e0eef562ace3a2cea403c1fb7021/tornado-6.5.1-cp39-abi3-win32.whl", hash = "sha256:e0a36e1bc684dca10b1aa75a31df8bdfed656831489bc1e6a6ebed05dc1ec365", size = 444396, upload-time = "2025-05-22T18:15:34.205Z" }, + { url = "https://files.pythonhosted.org/packages/7b/09/6526e32bf1049ee7de3bebba81572673b19a2a8541f795d887e92af1a8bc/tornado-6.5.1-cp39-abi3-win_amd64.whl", hash = "sha256:908e7d64567cecd4c2b458075589a775063453aeb1d2a1853eedb806922f568b", size = 444840, upload-time = "2025-05-22T18:15:36.1Z" }, + { url = "https://files.pythonhosted.org/packages/55/a7/535c44c7bea4578e48281d83c615219f3ab19e6abc67625ef637c73987be/tornado-6.5.1-cp39-abi3-win_arm64.whl", hash = "sha256:02420a0eb7bf617257b9935e2b754d1b63897525d8a289c9d65690d580b4dcf7", size = 443596, upload-time = "2025-05-22T18:15:37.433Z" }, +] + +[[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 = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.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 = "wcwidth" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" }, +] + +[[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" }, +]